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",
"name": "SWE-IHOR",
"ip": "94.142.140.1",
"provider": "cloudflare",
"country": "SWE",
"description": "Cloudflare Sweden - основной выход",
"serverId": "SWE-HIPHOST"
},
@@ -13,8 +11,6 @@
"id": "gw-bunny-swe",
"name": "SWE-BUNNY",
"ip": "94.142.140.2",
"provider": "bunny",
"country": "SWE",
"description": "Bunny CDN Sweden",
"serverId": "SWE-HIPHOST"
},
@@ -22,8 +18,6 @@
"id": "gw-telegram-nl",
"name": "TG-PROXY-1",
"ip": "149.154.167.50",
"provider": "telegram",
"country": "NL",
"description": "Telegram DC2",
"serverId": "NL-AMSTERDAM"
},
@@ -31,8 +25,6 @@
"id": "gw-hetzner-de",
"name": "DE-HETZNER",
"ip": "195.100.30.21",
"provider": "hetzner",
"country": "DE",
"description": "Hetzner Frankfurt",
"serverId": "DE-FRANKFURT"
},
@@ -40,8 +32,6 @@
"id": "gw-fastly-us",
"name": "US-FASTLY",
"ip": "151.101.1.1",
"provider": "fastly",
"country": "US",
"description": "Fastly US East",
"serverId": "US-NEWYORK"
}
+123 -95
View File
@@ -55,8 +55,6 @@ const getEmptyGateway = () => ({
id: `gw-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`,
name: '',
ip: '',
provider: 'custom',
country: '',
description: '',
serverId: '',
});
@@ -198,7 +196,11 @@ function NetworkConfigManager() {
let result = [...(config.gateways || [])];
if (providerFilter) {
result = result.filter(g => g.provider === providerFilter);
// Фильтруем по провайдеру сервера
result = result.filter(g => {
const server = getServerInfo(g.serverId);
return server?.provider === providerFilter;
});
}
if (serverFilter) {
@@ -207,13 +209,17 @@ function NetworkConfigManager() {
if (searchTerm) {
const term = searchTerm.toLowerCase();
result = result.filter(g =>
g.name?.toLowerCase().includes(term) ||
g.ip?.toLowerCase().includes(term) ||
g.country?.toLowerCase().includes(term) ||
g.description?.toLowerCase().includes(term) ||
getServerLabel(g.serverId).toLowerCase().includes(term)
);
result = result.filter(g => {
const server = getServerInfo(g.serverId);
return (
g.name?.toLowerCase().includes(term) ||
g.ip?.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;
@@ -442,8 +448,19 @@ function NetworkConfigManager() {
// === Получение цвета провайдера ===
const getProviderColor = (provider) => {
const p = GATEWAY_PROVIDERS.find(gp => gp.value === provider);
return p?.color || 'secondary';
if (!provider) return '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) => {
@@ -501,66 +518,72 @@ function NetworkConfigManager() {
};
// === Рендер карточки Gateway ===
const renderGatewayCard = (gateway) => (
<div key={gateway.id} className="col-sm-6 col-lg-4">
<div className="card card-sm">
<div className="card-body">
<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}
const renderGatewayCard = (gateway) => {
const server = getServerInfo(gateway.serverId);
const provider = server?.provider || '';
const country = server?.country || '';
return (
<div key={gateway.id} className="col-sm-6 col-lg-4">
<div className="card card-sm">
<div className="card-body">
<div className="d-flex align-items-center mb-2">
<span className={`avatar avatar-sm bg-${getProviderColor(provider)}-lt me-2`}>
<IconWorld size={16} className={`text-${getProviderColor(provider)}`} />
</span>
<div className="flex-fill">
<div className="fw-medium">{gateway.name || '—'}</div>
<div className="text-muted small">
{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="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" />Редактировать
<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>
<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>
{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>
{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>
);
);
};
// === Рендер карточки Interface ===
const renderInterfaceCard = (iface) => (
@@ -739,7 +762,7 @@ function NetworkConfigManager() {
</div>
)}
{/* Provider filter for gateways */}
{/* Provider filter for gateways - берем уникальные провайдеры из серверов */}
{activeTab === 'gateways' && (
<div className="col-6 col-md-3 col-lg-2">
<select
@@ -748,8 +771,8 @@ function NetworkConfigManager() {
onChange={(e) => setProviderFilter(e.target.value)}
>
<option value="">Все провайдеры</option>
{GATEWAY_PROVIDERS.map(p => (
<option key={p.value} value={p.value}>{p.label}</option>
{Array.from(new Set(servers.map(s => s.provider).filter(Boolean))).map(provider => (
<option key={provider} value={provider}>{provider}</option>
))}
</select>
</div>
@@ -887,12 +910,15 @@ function NetworkConfigManager() {
<tbody>
{filteredGateways.map(gateway => {
const server = getServerInfo(gateway.serverId);
const provider = server?.provider || '';
const country = server?.country || '';
return (
<tr key={gateway.id}>
<td>
<div className="d-flex align-items-center">
<span className={`avatar avatar-xs bg-${getProviderColor(gateway.provider)}-lt me-2`}>
<IconCircleFilled size={8} className={`text-${getProviderColor(gateway.provider)}`} />
<span className={`avatar avatar-xs bg-${getProviderColor(provider)}-lt me-2`}>
<IconCircleFilled size={8} className={`text-${getProviderColor(provider)}`} />
</span>
<strong>{gateway.name || '—'}</strong>
</div>
@@ -912,9 +938,11 @@ function NetworkConfigManager() {
</code>
</td>
<td>
<span className={`badge bg-${getProviderColor(gateway.provider)}-lt text-${getProviderColor(gateway.provider)}`}>
{GATEWAY_PROVIDERS.find(p => p.value === gateway.provider)?.label || gateway.provider}
</span>
{provider ? (
<span className={`badge bg-${getProviderColor(provider)}-lt text-${getProviderColor(provider)}`}>
{provider}
</span>
) : '—'}
</td>
<td>
{server ? (
@@ -929,8 +957,8 @@ function NetworkConfigManager() {
)}
</td>
<td>
{gateway.country ? (
<span>{countryToFlag(gateway.country)} {gateway.country}</span>
{country ? (
<span>{countryToFlag(country)} {country}</span>
) : '—'}
</td>
<td className="text-end">
@@ -1173,14 +1201,16 @@ function NetworkConfigManager() {
{editingGateway && (
<div className="row g-3">
<div className="col-12">
<label className="form-label">Сервер</label>
<label className="form-label required">Сервер</label>
<ServerAutocompleteInput
value={editingGateway.serverId}
onChange={(val) => setEditingGateway({ ...editingGateway, serverId: val })}
servers={servers}
placeholder="Выберите сервер..."
/>
<div className="form-text">Gateway будет привязан к выбранному серверу</div>
<div className="form-text">
Страна и провайдер будут автоматически взяты из данных сервера
</div>
</div>
<div className="col-md-6">
<FormField
@@ -1201,25 +1231,6 @@ function NetworkConfigManager() {
placeholder="94.142.140.1"
/>
</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">
<FormField
label="Описание"
@@ -1229,6 +1240,23 @@ function NetworkConfigManager() {
placeholder="Основной выход через Cloudflare"
/>
</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>
)}
</FormModal>