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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m38s
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`}>
|
return (
|
||||||
<IconWorld size={16} className={`text-${getProviderColor(gateway.provider)}`} />
|
<div key={gateway.id} className="col-sm-6 col-lg-4">
|
||||||
</span>
|
<div className="card card-sm">
|
||||||
<div className="flex-fill">
|
<div className="card-body">
|
||||||
<div className="fw-medium">{gateway.name || '—'}</div>
|
<div className="d-flex align-items-center mb-2">
|
||||||
<div className="text-muted small">
|
<span className={`avatar avatar-sm bg-${getProviderColor(provider)}-lt me-2`}>
|
||||||
{GATEWAY_PROVIDERS.find(p => p.value === gateway.provider)?.label || gateway.provider}
|
<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>
|
</div>
|
||||||
<div className="dropdown">
|
|
||||||
<button className="btn btn-ghost-secondary btn-icon btn-sm" data-bs-toggle="dropdown">
|
<div className="d-flex align-items-center gap-2 mb-2">
|
||||||
<IconEdit size={16} />
|
<code className="flex-fill text-truncate" title={gateway.ip}>
|
||||||
</button>
|
{gateway.ip || '—'}
|
||||||
<div className="dropdown-menu dropdown-menu-end">
|
</code>
|
||||||
<button className="dropdown-item" onClick={() => handleEditGateway(gateway)}>
|
{gateway.ip && (
|
||||||
<IconEdit size={16} className="me-2" />Редактировать
|
<button
|
||||||
|
className="btn btn-ghost-secondary btn-icon btn-sm"
|
||||||
|
onClick={() => copyToClipboard(gateway.ip)}
|
||||||
|
title="Копировать IP"
|
||||||
|
>
|
||||||
|
<IconCopy size={14} />
|
||||||
</button>
|
</button>
|
||||||
<button className="dropdown-item text-danger" onClick={() => handleDeleteGateway(gateway)}>
|
)}
|
||||||
<IconTrash size={16} className="me-2" />Удалить
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{country && (
|
||||||
<div className="d-flex align-items-center gap-2 mb-2">
|
<div className="d-flex align-items-center gap-1 text-muted small">
|
||||||
<code className="flex-fill text-truncate" title={gateway.ip}>
|
<span>{countryToFlag(country)}</span>
|
||||||
{gateway.ip || '—'}
|
<span>{country}</span>
|
||||||
</code>
|
</div>
|
||||||
{gateway.ip && (
|
)}
|
||||||
<button
|
|
||||||
className="btn btn-ghost-secondary btn-icon btn-sm"
|
{gateway.description && (
|
||||||
onClick={() => copyToClipboard(gateway.ip)}
|
<div className="text-muted small mt-1 text-truncate" title={gateway.description}>
|
||||||
title="Копировать IP"
|
{gateway.description}
|
||||||
>
|
</div>
|
||||||
<IconCopy size={14} />
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user