feat: Refactor EasySwitchManager to streamline data loading and enhance community and gateway management, including improved error handling and UI updates for better user experience.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m54s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m54s
This commit is contained in:
@@ -186,6 +186,21 @@ GET /api/servers // Метаданные серверов
|
|||||||
- activeTab: string // Активная вкладка (proxies/providers)
|
- activeTab: string // Активная вкладка (proxies/providers)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Важные изменения (v2)
|
||||||
|
|
||||||
|
### ✅ Исправлено в версии 2.0
|
||||||
|
- **Все communities отображаются для всех серверов** - больше нет различий в количестве communities между серверами
|
||||||
|
- **Загрузка названий communities** из справочника `/api/communities`
|
||||||
|
- **Все gateways сервера** отображаются для каждого community, а не только те, что настроены в filters
|
||||||
|
- **Бейдж "настроен"** показывает, для каких communities уже есть фильтры
|
||||||
|
- **Активный gateway берется из filters**, если настроен, иначе выбирается первый
|
||||||
|
|
||||||
|
### Логика работы v2
|
||||||
|
1. Загружается справочник communities → **все одинаковые для всех серверов**
|
||||||
|
2. Для каждого сервера загружаются его gateways (из `/api/servers` или `server-configs`)
|
||||||
|
3. Для каждого community показываются **все** gateways этого сервера
|
||||||
|
4. Из filters берется информация только об **активном** gateway для данного community
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
### Планируется добавить
|
### Планируется добавить
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ function EasySwitchManager() {
|
|||||||
// Серверы и фильтры
|
// Серверы и фильтры
|
||||||
const [servers, setServers] = useState([]);
|
const [servers, setServers] = useState([]);
|
||||||
const [inventoryServers, setInventoryServers] = useState([]);
|
const [inventoryServers, setInventoryServers] = useState([]);
|
||||||
|
const [communitiesDirectory, setCommunitiesDirectory] = useState([]); // Справочник communities
|
||||||
|
|
||||||
// Поиск и фильтры
|
// Поиск и фильтры
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
@@ -41,20 +42,29 @@ function EasySwitchManager() {
|
|||||||
const [hasChanges, setHasChanges] = useState(false);
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadData();
|
const initData = async () => {
|
||||||
loadInventoryServers();
|
try {
|
||||||
|
// Загружаем communities
|
||||||
|
const commRes = await api.get(`/communities`);
|
||||||
|
const communities = Array.isArray(commRes.data) ? commRes.data : [];
|
||||||
|
setCommunitiesDirectory(communities);
|
||||||
|
|
||||||
|
// Загружаем inventory servers
|
||||||
|
const invRes = await api.get(`/servers`);
|
||||||
|
const inventory = Array.isArray(invRes.data) ? invRes.data : [];
|
||||||
|
setInventoryServers(inventory);
|
||||||
|
|
||||||
|
// Загружаем данные серверов и фильтров
|
||||||
|
await loadDataWithParams(communities, inventory);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error initializing data:', e);
|
||||||
|
setError('Ошибка загрузки данных');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
initData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadInventoryServers = async () => {
|
const loadDataWithParams = async (communities, inventory) => {
|
||||||
try {
|
|
||||||
const res = await api.get(`/servers`);
|
|
||||||
setInventoryServers(Array.isArray(res.data) ? res.data : []);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error loading inventory servers:', e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadData = async () => {
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
@@ -63,60 +73,84 @@ function EasySwitchManager() {
|
|||||||
const serversResponse = await api.get(`/server-configs`);
|
const serversResponse = await api.get(`/server-configs`);
|
||||||
const serversList = Array.isArray(serversResponse.data) ? serversResponse.data : [];
|
const serversList = Array.isArray(serversResponse.data) ? serversResponse.data : [];
|
||||||
|
|
||||||
// Загружаем фильтры для каждого сервера
|
// Загружаем фильтры для каждого сервера (для определения активных gateways)
|
||||||
const serversWithFilters = await Promise.all(
|
const serversWithData = await Promise.all(
|
||||||
serversList.map(async (server) => {
|
serversList.map(async (server) => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get(`/server-filters/${server.id}`);
|
// Загружаем filters для сервера
|
||||||
const filters = Array.isArray(response.data) ? response.data : [];
|
const filtersResponse = await api.get(`/server-filters/${server.id}`);
|
||||||
|
const filters = Array.isArray(filtersResponse.data) ? filtersResponse.data : [];
|
||||||
|
|
||||||
// Группируем фильтры по community внутри сервера
|
// Получаем gateways сервера из inventory или server-configs
|
||||||
const communitiesMap = new Map();
|
const matchedInventoryServer = inventory.find(srv =>
|
||||||
|
String(srv.dns || '').trim() === server.name ||
|
||||||
|
String(srv.hostName || '').trim() === server.name ||
|
||||||
|
String(srv.ip || '').trim() === server.name
|
||||||
|
);
|
||||||
|
|
||||||
|
let serverGateways = [];
|
||||||
|
|
||||||
|
// Берем gateways из inventory
|
||||||
|
if (matchedInventoryServer && Array.isArray(matchedInventoryServer.gateways)) {
|
||||||
|
serverGateways = matchedInventoryServer.gateways
|
||||||
|
.filter(gw => gw.name && gw.name !== server.name)
|
||||||
|
.map(gw => ({
|
||||||
|
name: gw.name,
|
||||||
|
ip: gw.ip || '',
|
||||||
|
comment: gw.comment || '',
|
||||||
|
primary: gw.primary || false
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если нет в inventory, берем из server-configs
|
||||||
|
if (serverGateways.length === 0 && Array.isArray(server.gateways)) {
|
||||||
|
serverGateways = server.gateways
|
||||||
|
.filter(gw => gw.name && gw.name !== server.name)
|
||||||
|
.map(gw => ({
|
||||||
|
name: gw.name,
|
||||||
|
ip: gw.ip || '',
|
||||||
|
comment: gw.comment || '',
|
||||||
|
primary: gw.primary || false
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем map фильтров по community для быстрого поиска
|
||||||
|
const filtersMap = new Map();
|
||||||
filters.forEach(filter => {
|
filters.forEach(filter => {
|
||||||
const comm = filter.community;
|
filtersMap.set(filter.community, filter.gateway);
|
||||||
if (!communitiesMap.has(comm)) {
|
|
||||||
communitiesMap.set(comm, {
|
|
||||||
community: comm,
|
|
||||||
description: filter.description || '',
|
|
||||||
gateways: []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const communityData = communitiesMap.get(comm);
|
|
||||||
// Добавляем gateway, если его еще нет
|
|
||||||
if (!communityData.gateways.find(g => g.name === filter.gateway)) {
|
|
||||||
communityData.gateways.push({
|
|
||||||
name: filter.gateway,
|
|
||||||
description: filter.description
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...server,
|
...server,
|
||||||
communities: Array.from(communitiesMap.values())
|
gateways: serverGateways,
|
||||||
|
filtersMap: filtersMap
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Error loading filters for ${server.id}:`, e);
|
console.error(`Error loading data for ${server.id}:`, e);
|
||||||
return {
|
return {
|
||||||
...server,
|
...server,
|
||||||
communities: []
|
gateways: [],
|
||||||
|
filtersMap: new Map()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Фильтруем серверы, у которых есть communities
|
// Фильтруем серверы, у которых есть gateways
|
||||||
const serversWithCommunities = serversWithFilters.filter(s => s.communities.length > 0);
|
const serversWithGateways = serversWithData.filter(s => s.gateways.length > 0);
|
||||||
setServers(serversWithCommunities);
|
setServers(serversWithGateways);
|
||||||
|
|
||||||
// Устанавливаем активные шлюзы (первый gateway для каждого community на каждом сервере)
|
// Устанавливаем активные шлюзы из filters или первый gateway
|
||||||
const initialActive = {};
|
const initialActive = {};
|
||||||
serversWithCommunities.forEach(server => {
|
serversWithGateways.forEach(server => {
|
||||||
server.communities.forEach(comm => {
|
communities.forEach(comm => {
|
||||||
if (comm.gateways.length > 0) {
|
const key = `${server.id}:${comm.value}`;
|
||||||
const key = `${server.id}:${comm.community}`;
|
// Берем gateway из filters, если есть, иначе первый доступный
|
||||||
initialActive[key] = comm.gateways[0].name;
|
const gatewayFromFilter = server.filtersMap.get(comm.value);
|
||||||
|
if (gatewayFromFilter && server.gateways.find(g => g.name === gatewayFromFilter)) {
|
||||||
|
initialActive[key] = gatewayFromFilter;
|
||||||
|
} else if (server.gateways.length > 0) {
|
||||||
|
initialActive[key] = server.gateways[0].name;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -146,9 +180,24 @@ function EasySwitchManager() {
|
|||||||
// Здесь можно добавить логику обновления фильтров на серверах
|
// Здесь можно добавить логику обновления фильтров на серверах
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = async () => {
|
||||||
loadData();
|
try {
|
||||||
loadInventoryServers();
|
// Загружаем communities
|
||||||
|
const commRes = await api.get(`/communities`);
|
||||||
|
const communities = Array.isArray(commRes.data) ? commRes.data : [];
|
||||||
|
setCommunitiesDirectory(communities);
|
||||||
|
|
||||||
|
// Загружаем inventory servers
|
||||||
|
const invRes = await api.get(`/servers`);
|
||||||
|
const inventory = Array.isArray(invRes.data) ? invRes.data : [];
|
||||||
|
setInventoryServers(inventory);
|
||||||
|
|
||||||
|
// Загружаем данные серверов и фильтров
|
||||||
|
await loadDataWithParams(communities, inventory);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error refreshing data:', e);
|
||||||
|
setError('Ошибка обновления данных');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Получаем метаданные сервера из inventory
|
// Получаем метаданные сервера из inventory
|
||||||
@@ -172,14 +221,15 @@ function EasySwitchManager() {
|
|||||||
if (serverName.includes(search) || serverDesc.includes(search)) return true;
|
if (serverName.includes(search) || serverDesc.includes(search)) return true;
|
||||||
|
|
||||||
// Поиск по communities и gateways
|
// Поиск по communities и gateways
|
||||||
return server.communities.some(comm => {
|
return communitiesDirectory.some(comm => {
|
||||||
const commValue = (comm.community || '').toLowerCase();
|
const commValue = (comm.value || '').toLowerCase();
|
||||||
|
const commName = (comm.name || '').toLowerCase();
|
||||||
const commDesc = (comm.description || '').toLowerCase();
|
const commDesc = (comm.description || '').toLowerCase();
|
||||||
|
|
||||||
return commValue.includes(search) ||
|
return commValue.includes(search) ||
|
||||||
commDesc.includes(search) ||
|
commName.includes(search) ||
|
||||||
comm.gateways.some(g => (g.name || '').toLowerCase().includes(search));
|
commDesc.includes(search);
|
||||||
});
|
}) || server.gateways.some(g => (g.name || '').toLowerCase().includes(search));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loading && servers.length === 0) {
|
if (loading && servers.length === 0) {
|
||||||
@@ -220,7 +270,7 @@ function EasySwitchManager() {
|
|||||||
Easy Switch
|
Easy Switch
|
||||||
</h2>
|
</h2>
|
||||||
<div className="text-muted mt-1">
|
<div className="text-muted mt-1">
|
||||||
Быстрое переключение шлюзов для {servers.length} {servers.length === 1 ? 'сервера' : 'серверов'}
|
Быстрое переключение шлюзов • {servers.length} {servers.length === 1 ? 'сервер' : 'серверов'} • {communitiesDirectory.length} communities
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-auto">
|
<div className="col-auto">
|
||||||
@@ -256,7 +306,7 @@ function EasySwitchManager() {
|
|||||||
onClick={() => setActiveTab('proxies')}
|
onClick={() => setActiveTab('proxies')}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Прокси ({servers.reduce((sum, s) => sum + s.communities.length, 0)})
|
Прокси ({communitiesDirectory.length})
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li className="nav-item" role="presentation">
|
<li className="nav-item" role="presentation">
|
||||||
@@ -363,33 +413,39 @@ function EasySwitchManager() {
|
|||||||
<span className="text-muted small">— {server.description}</span>
|
<span className="text-muted small">— {server.description}</span>
|
||||||
)}
|
)}
|
||||||
<span className="badge bg-blue-lt text-blue ms-auto">
|
<span className="badge bg-blue-lt text-blue ms-auto">
|
||||||
{server.communities.length} {server.communities.length === 1 ? 'community' : 'communities'}
|
{communitiesDirectory.length} communities • {server.gateways.length} gateways
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card-body p-3">
|
<div className="card-body p-3">
|
||||||
{/* Communities для этого сервера */}
|
{/* ВСЕ Communities для этого сервера */}
|
||||||
{server.communities.map((comm) => {
|
{communitiesDirectory.map((comm) => {
|
||||||
const activeKey = `${server.id}:${comm.community}`;
|
const activeKey = `${server.id}:${comm.value}`;
|
||||||
const activeGw = activeGateways[activeKey];
|
const activeGw = activeGateways[activeKey];
|
||||||
|
const hasFilterForComm = server.filtersMap.has(comm.value);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={comm.community} className="mb-3">
|
<div key={comm.value} className="mb-3">
|
||||||
{/* Заголовок community */}
|
{/* Заголовок community */}
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<span className="badge bg-blue-lt text-blue me-2" style={{ fontSize: '0.8rem' }}>
|
<span className="badge bg-blue-lt text-blue me-2" style={{ fontSize: '0.8rem' }}>
|
||||||
{comm.community}
|
{comm.value}
|
||||||
</span>
|
</span>
|
||||||
<span className="fw-medium">{comm.description}</span>
|
<span className="fw-medium">{comm.name || comm.description || ''}</span>
|
||||||
|
{hasFilterForComm && (
|
||||||
|
<span className="badge bg-green-lt text-green ms-2" style={{ fontSize: '0.7rem' }}>
|
||||||
|
настроен
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span className="text-muted small ms-2">
|
<span className="text-muted small ms-2">
|
||||||
({comm.gateways.length})
|
({server.gateways.length})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Сетка карточек шлюзов */}
|
{/* Сетка карточек ВСЕх шлюзов сервера */}
|
||||||
<div className="row g-2">
|
<div className="row g-2">
|
||||||
{comm.gateways.map((gw, idx) => {
|
{server.gateways.map((gw, idx) => {
|
||||||
const isActive = activeGw === gw.name;
|
const isActive = activeGw === gw.name;
|
||||||
const isFastest = idx === 0;
|
const isFastest = idx === 0;
|
||||||
const ping = 20 + idx * 10 + Math.floor(Math.random() * 15);
|
const ping = 20 + idx * 10 + Math.floor(Math.random() * 15);
|
||||||
@@ -405,7 +461,7 @@ function EasySwitchManager() {
|
|||||||
borderRadius: '6px',
|
borderRadius: '6px',
|
||||||
boxShadow: isActive ? '0 3px 8px rgba(32, 107, 196, 0.3)' : 'none'
|
boxShadow: isActive ? '0 3px 8px rgba(32, 107, 196, 0.3)' : 'none'
|
||||||
}}
|
}}
|
||||||
onClick={() => handleGatewaySelect(server.id, comm.community, gw.name)}
|
onClick={() => handleGatewaySelect(server.id, comm.value, gw.name)}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
if (!isActive) {
|
if (!isActive) {
|
||||||
e.currentTarget.style.boxShadow = '0 2px 6px rgba(0,0,0,0.1)';
|
e.currentTarget.style.boxShadow = '0 2px 6px rgba(0,0,0,0.1)';
|
||||||
|
|||||||
Reference in New Issue
Block a user