refactor(NetworkConfigManager): remove provider and country from gateway configuration; integrate server details for improved filtering and UI display
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m38s

This commit is contained in:
2026-01-22 10:32:54 +07:00
parent c04b2d726b
commit 48419d577a
2 changed files with 123 additions and 105 deletions
-10
View File
@@ -4,8 +4,6 @@
"id": "gw-cloudflare-swe", "id": "gw-cloudflare-swe",
"name": "SWE-IHOR", "name": "SWE-IHOR",
"ip": "94.142.140.1", "ip": "94.142.140.1",
"provider": "cloudflare",
"country": "SWE",
"description": "Cloudflare Sweden - основной выход", "description": "Cloudflare Sweden - основной выход",
"serverId": "SWE-HIPHOST" "serverId": "SWE-HIPHOST"
}, },
@@ -13,8 +11,6 @@
"id": "gw-bunny-swe", "id": "gw-bunny-swe",
"name": "SWE-BUNNY", "name": "SWE-BUNNY",
"ip": "94.142.140.2", "ip": "94.142.140.2",
"provider": "bunny",
"country": "SWE",
"description": "Bunny CDN Sweden", "description": "Bunny CDN Sweden",
"serverId": "SWE-HIPHOST" "serverId": "SWE-HIPHOST"
}, },
@@ -22,8 +18,6 @@
"id": "gw-telegram-nl", "id": "gw-telegram-nl",
"name": "TG-PROXY-1", "name": "TG-PROXY-1",
"ip": "149.154.167.50", "ip": "149.154.167.50",
"provider": "telegram",
"country": "NL",
"description": "Telegram DC2", "description": "Telegram DC2",
"serverId": "NL-AMSTERDAM" "serverId": "NL-AMSTERDAM"
}, },
@@ -31,8 +25,6 @@
"id": "gw-hetzner-de", "id": "gw-hetzner-de",
"name": "DE-HETZNER", "name": "DE-HETZNER",
"ip": "195.100.30.21", "ip": "195.100.30.21",
"provider": "hetzner",
"country": "DE",
"description": "Hetzner Frankfurt", "description": "Hetzner Frankfurt",
"serverId": "DE-FRANKFURT" "serverId": "DE-FRANKFURT"
}, },
@@ -40,8 +32,6 @@
"id": "gw-fastly-us", "id": "gw-fastly-us",
"name": "US-FASTLY", "name": "US-FASTLY",
"ip": "151.101.1.1", "ip": "151.101.1.1",
"provider": "fastly",
"country": "US",
"description": "Fastly US East", "description": "Fastly US East",
"serverId": "US-NEWYORK" "serverId": "US-NEWYORK"
} }
+126 -98
View File
@@ -55,8 +55,6 @@ const getEmptyGateway = () => ({
id: `gw-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`, id: `gw-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`,
name: '', name: '',
ip: '', ip: '',
provider: 'custom',
country: '',
description: '', description: '',
serverId: '', serverId: '',
}); });
@@ -198,7 +196,11 @@ function NetworkConfigManager() {
let result = [...(config.gateways || [])]; let result = [...(config.gateways || [])];
if (providerFilter) { if (providerFilter) {
result = result.filter(g => g.provider === providerFilter); // Фильтруем по провайдеру сервера
result = result.filter(g => {
const server = getServerInfo(g.serverId);
return server?.provider === providerFilter;
});
} }
if (serverFilter) { if (serverFilter) {
@@ -207,13 +209,17 @@ function NetworkConfigManager() {
if (searchTerm) { if (searchTerm) {
const term = searchTerm.toLowerCase(); const term = searchTerm.toLowerCase();
result = result.filter(g => result = result.filter(g => {
g.name?.toLowerCase().includes(term) || const server = getServerInfo(g.serverId);
g.ip?.toLowerCase().includes(term) || return (
g.country?.toLowerCase().includes(term) || g.name?.toLowerCase().includes(term) ||
g.description?.toLowerCase().includes(term) || g.ip?.toLowerCase().includes(term) ||
getServerLabel(g.serverId).toLowerCase().includes(term) server?.country?.toLowerCase().includes(term) ||
); server?.provider?.toLowerCase().includes(term) ||
g.description?.toLowerCase().includes(term) ||
getServerLabel(g.serverId).toLowerCase().includes(term)
);
});
} }
return result; return result;
@@ -442,8 +448,19 @@ function NetworkConfigManager() {
// === Получение цвета провайдера === // === Получение цвета провайдера ===
const getProviderColor = (provider) => { const getProviderColor = (provider) => {
const p = GATEWAY_PROVIDERS.find(gp => gp.value === provider); if (!provider) return 'secondary';
return p?.color || 'secondary'; // Ищем точное совпадение
const exact = GATEWAY_PROVIDERS.find(gp =>
gp.value === provider ||
gp.label.toLowerCase() === provider.toLowerCase()
);
if (exact) return exact.color;
// Проверяем частичное совпадение
const partial = GATEWAY_PROVIDERS.find(gp =>
provider.toLowerCase().includes(gp.value.toLowerCase()) ||
provider.toLowerCase().includes(gp.label.toLowerCase())
);
return partial?.color || 'secondary';
}; };
const getInterfaceTypeColor = (type) => { const getInterfaceTypeColor = (type) => {
@@ -501,66 +518,72 @@ function NetworkConfigManager() {
}; };
// === Рендер карточки Gateway === // === Рендер карточки Gateway ===
const renderGatewayCard = (gateway) => ( const renderGatewayCard = (gateway) => {
<div key={gateway.id} className="col-sm-6 col-lg-4"> const server = getServerInfo(gateway.serverId);
<div className="card card-sm"> const provider = server?.provider || '';
<div className="card-body"> const country = server?.country || '';
<div className="d-flex align-items-center mb-2">
<span className={`avatar avatar-sm bg-${getProviderColor(gateway.provider)}-lt me-2`}>
<IconWorld size={16} className={`text-${getProviderColor(gateway.provider)}`} />
</span>
<div className="flex-fill">
<div className="fw-medium">{gateway.name || '—'}</div>
<div className="text-muted small">
{GATEWAY_PROVIDERS.find(p => p.value === gateway.provider)?.label || gateway.provider}
</div>
</div>
<div className="dropdown">
<button className="btn btn-ghost-secondary btn-icon btn-sm" data-bs-toggle="dropdown">
<IconEdit size={16} />
</button>
<div className="dropdown-menu dropdown-menu-end">
<button className="dropdown-item" onClick={() => handleEditGateway(gateway)}>
<IconEdit size={16} className="me-2" />Редактировать
</button>
<button className="dropdown-item text-danger" onClick={() => handleDeleteGateway(gateway)}>
<IconTrash size={16} className="me-2" />Удалить
</button>
</div>
</div>
</div>
<div className="d-flex align-items-center gap-2 mb-2"> return (
<code className="flex-fill text-truncate" title={gateway.ip}> <div key={gateway.id} className="col-sm-6 col-lg-4">
{gateway.ip || '—'} <div className="card card-sm">
</code> <div className="card-body">
{gateway.ip && ( <div className="d-flex align-items-center mb-2">
<button <span className={`avatar avatar-sm bg-${getProviderColor(provider)}-lt me-2`}>
className="btn btn-ghost-secondary btn-icon btn-sm" <IconWorld size={16} className={`text-${getProviderColor(provider)}`} />
onClick={() => copyToClipboard(gateway.ip)} </span>
title="Копировать IP" <div className="flex-fill">
> <div className="fw-medium">{gateway.name || '—'}</div>
<IconCopy size={14} /> <div className="text-muted small">
</button> {provider || '—'}
</div>
</div>
<div className="dropdown">
<button className="btn btn-ghost-secondary btn-icon btn-sm" data-bs-toggle="dropdown">
<IconEdit size={16} />
</button>
<div className="dropdown-menu dropdown-menu-end">
<button className="dropdown-item" onClick={() => handleEditGateway(gateway)}>
<IconEdit size={16} className="me-2" />Редактировать
</button>
<button className="dropdown-item text-danger" onClick={() => handleDeleteGateway(gateway)}>
<IconTrash size={16} className="me-2" />Удалить
</button>
</div>
</div>
</div>
<div className="d-flex align-items-center gap-2 mb-2">
<code className="flex-fill text-truncate" title={gateway.ip}>
{gateway.ip || '—'}
</code>
{gateway.ip && (
<button
className="btn btn-ghost-secondary btn-icon btn-sm"
onClick={() => copyToClipboard(gateway.ip)}
title="Копировать IP"
>
<IconCopy size={14} />
</button>
)}
</div>
{country && (
<div className="d-flex align-items-center gap-1 text-muted small">
<span>{countryToFlag(country)}</span>
<span>{country}</span>
</div>
)}
{gateway.description && (
<div className="text-muted small mt-1 text-truncate" title={gateway.description}>
{gateway.description}
</div>
)} )}
</div> </div>
{gateway.country && (
<div className="d-flex align-items-center gap-1 text-muted small">
<span>{countryToFlag(gateway.country)}</span>
<span>{gateway.country}</span>
</div>
)}
{gateway.description && (
<div className="text-muted small mt-1 text-truncate" title={gateway.description}>
{gateway.description}
</div>
)}
</div> </div>
</div> </div>
</div> );
); };
// === Рендер карточки Interface === // === Рендер карточки Interface ===
const renderInterfaceCard = (iface) => ( const renderInterfaceCard = (iface) => (
@@ -739,7 +762,7 @@ function NetworkConfigManager() {
</div> </div>
)} )}
{/* Provider filter for gateways */} {/* Provider filter for gateways - берем уникальные провайдеры из серверов */}
{activeTab === 'gateways' && ( {activeTab === 'gateways' && (
<div className="col-6 col-md-3 col-lg-2"> <div className="col-6 col-md-3 col-lg-2">
<select <select
@@ -748,8 +771,8 @@ function NetworkConfigManager() {
onChange={(e) => setProviderFilter(e.target.value)} onChange={(e) => setProviderFilter(e.target.value)}
> >
<option value="">Все провайдеры</option> <option value="">Все провайдеры</option>
{GATEWAY_PROVIDERS.map(p => ( {Array.from(new Set(servers.map(s => s.provider).filter(Boolean))).map(provider => (
<option key={p.value} value={p.value}>{p.label}</option> <option key={provider} value={provider}>{provider}</option>
))} ))}
</select> </select>
</div> </div>
@@ -887,12 +910,15 @@ function NetworkConfigManager() {
<tbody> <tbody>
{filteredGateways.map(gateway => { {filteredGateways.map(gateway => {
const server = getServerInfo(gateway.serverId); const server = getServerInfo(gateway.serverId);
const provider = server?.provider || '';
const country = server?.country || '';
return ( return (
<tr key={gateway.id}> <tr key={gateway.id}>
<td> <td>
<div className="d-flex align-items-center"> <div className="d-flex align-items-center">
<span className={`avatar avatar-xs bg-${getProviderColor(gateway.provider)}-lt me-2`}> <span className={`avatar avatar-xs bg-${getProviderColor(provider)}-lt me-2`}>
<IconCircleFilled size={8} className={`text-${getProviderColor(gateway.provider)}`} /> <IconCircleFilled size={8} className={`text-${getProviderColor(provider)}`} />
</span> </span>
<strong>{gateway.name || '—'}</strong> <strong>{gateway.name || '—'}</strong>
</div> </div>
@@ -912,9 +938,11 @@ function NetworkConfigManager() {
</code> </code>
</td> </td>
<td> <td>
<span className={`badge bg-${getProviderColor(gateway.provider)}-lt text-${getProviderColor(gateway.provider)}`}> {provider ? (
{GATEWAY_PROVIDERS.find(p => p.value === gateway.provider)?.label || gateway.provider} <span className={`badge bg-${getProviderColor(provider)}-lt text-${getProviderColor(provider)}`}>
</span> {provider}
</span>
) : '—'}
</td> </td>
<td> <td>
{server ? ( {server ? (
@@ -929,8 +957,8 @@ function NetworkConfigManager() {
)} )}
</td> </td>
<td> <td>
{gateway.country ? ( {country ? (
<span>{countryToFlag(gateway.country)} {gateway.country}</span> <span>{countryToFlag(country)} {country}</span>
) : '—'} ) : '—'}
</td> </td>
<td className="text-end"> <td className="text-end">
@@ -1173,14 +1201,16 @@ function NetworkConfigManager() {
{editingGateway && ( {editingGateway && (
<div className="row g-3"> <div className="row g-3">
<div className="col-12"> <div className="col-12">
<label className="form-label">Сервер</label> <label className="form-label required">Сервер</label>
<ServerAutocompleteInput <ServerAutocompleteInput
value={editingGateway.serverId} value={editingGateway.serverId}
onChange={(val) => setEditingGateway({ ...editingGateway, serverId: val })} onChange={(val) => setEditingGateway({ ...editingGateway, serverId: val })}
servers={servers} servers={servers}
placeholder="Выберите сервер..." placeholder="Выберите сервер..."
/> />
<div className="form-text">Gateway будет привязан к выбранному серверу</div> <div className="form-text">
Страна и провайдер будут автоматически взяты из данных сервера
</div>
</div> </div>
<div className="col-md-6"> <div className="col-md-6">
<FormField <FormField
@@ -1201,25 +1231,6 @@ function NetworkConfigManager() {
placeholder="94.142.140.1" placeholder="94.142.140.1"
/> />
</div> </div>
<div className="col-md-6">
<FormField
label="Провайдер"
name="provider"
type="select"
value={editingGateway.provider}
onChange={(val) => setEditingGateway({ ...editingGateway, provider: val })}
options={GATEWAY_PROVIDERS.map(p => ({ value: p.value, label: p.label }))}
/>
</div>
<div className="col-md-6">
<FormField
label="Страна"
name="country"
value={editingGateway.country}
onChange={(val) => setEditingGateway({ ...editingGateway, country: val })}
placeholder="SWE"
/>
</div>
<div className="col-12"> <div className="col-12">
<FormField <FormField
label="Описание" label="Описание"
@@ -1229,6 +1240,23 @@ function NetworkConfigManager() {
placeholder="Основной выход через Cloudflare" placeholder="Основной выход через Cloudflare"
/> />
</div> </div>
{editingGateway.serverId && (() => {
const server = getServerInfo(editingGateway.serverId);
return server ? (
<div className="col-12">
<div className="alert alert-info mb-0">
<div className="d-flex align-items-center">
<IconServer size={16} className="me-2" />
<div>
<strong>{server.dns || server.ip}</strong>
{server.provider && <span className="ms-2"> {server.provider}</span>}
{server.country && <span className="ms-2"> {countryToFlag(server.country)} {server.country}</span>}
</div>
</div>
</div>
</div>
) : null;
})()}
</div> </div>
)} )}
</FormModal> </FormModal>