feat(NetworkConfigManager): enhance gateway configuration with new fields for country and type; add recursive gateway support and improve filtering options
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m52s

This commit is contained in:
2026-01-22 10:59:50 +07:00
parent e5f576d11e
commit 1e6f5badec
2 changed files with 151 additions and 21 deletions
+25 -5
View File
@@ -5,35 +5,55 @@
"name": "SWE-IHOR",
"ip": "94.142.140.1",
"description": "Cloudflare Sweden - основной выход",
"serverId": "SWE-HIPHOST"
"serverId": "SWE-HIPHOST",
"country": "SWE",
"type": "direct"
},
{
"id": "gw-bunny-swe",
"name": "SWE-BUNNY",
"ip": "94.142.140.2",
"description": "Bunny CDN Sweden",
"serverId": "SWE-HIPHOST"
"serverId": "SWE-HIPHOST",
"country": "SWE",
"type": "direct"
},
{
"id": "gw-telegram-nl",
"name": "TG-PROXY-1",
"ip": "149.154.167.50",
"description": "Telegram DC2",
"serverId": "NL-AMSTERDAM"
"serverId": "NL-AMSTERDAM",
"country": "NL",
"type": "direct"
},
{
"id": "gw-hetzner-de",
"name": "DE-HETZNER",
"ip": "195.100.30.21",
"description": "Hetzner Frankfurt",
"serverId": "DE-FRANKFURT"
"serverId": "DE-FRANKFURT",
"country": "DE",
"type": "direct"
},
{
"id": "gw-fastly-us",
"name": "US-FASTLY",
"ip": "151.101.1.1",
"description": "Fastly US East",
"serverId": "US-NEWYORK"
"serverId": "US-NEWYORK",
"country": "US",
"type": "direct"
},
{
"id": "gw-recursive-swe",
"name": "SWE-RECURSIVE",
"ip": "10.0.0.1",
"description": "Рекурсивный gateway через SWE-IHOR",
"serverId": "SWE-HIPHOST",
"country": "SWE",
"type": "recursive",
"parentGatewayId": "gw-cloudflare-swe"
}
],
"tunnelInterfaces": [
+126 -16
View File
@@ -50,6 +50,12 @@ const INTERFACE_TYPES = [
{ value: 'OpenVPN', label: 'OpenVPN', color: 'orange' },
];
// Типы gateway
const GATEWAY_TYPES = [
{ value: 'direct', label: 'Прямой', description: 'Прямой доступ в интернет', color: 'green' },
{ value: 'recursive', label: 'Рекурсивный', description: 'Ссылается на другой gateway', color: 'blue' },
];
// Пустые объекты
const getEmptyGateway = () => ({
id: `gw-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`,
@@ -57,6 +63,9 @@ const getEmptyGateway = () => ({
ip: '',
description: '',
serverId: '',
country: '',
type: 'direct',
parentGatewayId: '', // Для рекурсивных - ID родительского gateway
});
const getEmptyInterface = () => ({
@@ -97,6 +106,7 @@ function NetworkConfigManager() {
const [providerFilter, setProviderFilter] = useState('');
const [serverFilter, setServerFilter] = useState('');
const [typeFilter, setTypeFilter] = useState('');
const [gatewayTypeFilter, setGatewayTypeFilter] = useState(''); // Для фильтрации gateway по типу
const [viewMode, setViewMode] = useState('cards'); // 'cards' | 'table'
// === Modals ===
@@ -207,6 +217,10 @@ function NetworkConfigManager() {
result = result.filter(g => g.serverId === serverFilter);
}
if (gatewayTypeFilter) {
result = result.filter(g => g.type === gatewayTypeFilter);
}
if (searchTerm) {
const term = searchTerm.toLowerCase();
result = result.filter(g => {
@@ -214,6 +228,7 @@ function NetworkConfigManager() {
return (
g.name?.toLowerCase().includes(term) ||
g.ip?.toLowerCase().includes(term) ||
g.country?.toLowerCase().includes(term) ||
server?.country?.toLowerCase().includes(term) ||
server?.provider?.toLowerCase().includes(term) ||
g.description?.toLowerCase().includes(term) ||
@@ -223,7 +238,7 @@ function NetworkConfigManager() {
}
return result;
}, [config.gateways, providerFilter, serverFilter, searchTerm, servers]);
}, [config.gateways, providerFilter, serverFilter, typeFilter, searchTerm, servers]);
const filteredInterfaces = useMemo(() => {
let result = [...(config.tunnelInterfaces || [])];
@@ -292,9 +307,10 @@ function NetworkConfigManager() {
setProviderFilter('');
setServerFilter('');
setTypeFilter('');
setGatewayTypeFilter('');
};
const hasActiveFilters = searchTerm || providerFilter || serverFilter || typeFilter;
const hasActiveFilters = searchTerm || providerFilter || serverFilter || typeFilter || gatewayTypeFilter;
// === CRUD для Gateways ===
const handleAddGateway = () => {
@@ -517,22 +533,39 @@ function NetworkConfigManager() {
);
};
// === Получение родительского gateway ===
const getParentGateway = (gatewayId) => {
if (!gatewayId) return null;
return config.gateways?.find(g => g.id === gatewayId);
};
// === Рендер карточки Gateway ===
const renderGatewayCard = (gateway) => {
const server = getServerInfo(gateway.serverId);
const provider = server?.provider || '';
const country = server?.country || '';
const serverCountry = server?.country || '';
const gatewayCountry = gateway.country || '';
const displayCountry = gatewayCountry || serverCountry;
const gatewayType = GATEWAY_TYPES.find(t => t.value === gateway.type) || GATEWAY_TYPES[0];
const parentGateway = gateway.type === 'recursive' && gateway.parentGatewayId
? getParentGateway(gateway.parentGatewayId)
: null;
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 className={`avatar avatar-sm bg-${gatewayType.color}-lt me-2`}>
<IconWorld size={16} className={`text-${gatewayType.color}`} />
</span>
<div className="flex-fill">
<div className="fw-medium">{gateway.name || '—'}</div>
<div className="d-flex align-items-center gap-1">
<span className="fw-medium">{gateway.name || '—'}</span>
<span className={`badge bg-${gatewayType.color}-lt text-${gatewayType.color}`} style={{ fontSize: '0.65rem' }}>
{gatewayType.label}
</span>
</div>
<div className="text-muted small">
{provider || '—'}
</div>
@@ -567,10 +600,20 @@ function NetworkConfigManager() {
)}
</div>
{country && (
<div className="d-flex align-items-center gap-1 text-muted small">
<span>{countryToFlag(country)}</span>
<span>{country}</span>
{displayCountry && (
<div className="d-flex align-items-center gap-1 text-muted small mb-1">
<span>{countryToFlag(displayCountry)}</span>
<span>{displayCountry}</span>
{gatewayCountry && serverCountry && gatewayCountry !== serverCountry && (
<span className="text-muted" style={{ fontSize: '0.7rem' }}>(сервер: {serverCountry})</span>
)}
</div>
)}
{parentGateway && (
<div className="d-flex align-items-center gap-1 text-muted small mb-1">
<span className="text-muted"></span>
<span>Родитель: {parentGateway.name}</span>
</div>
)}
@@ -906,6 +949,7 @@ function NetworkConfigManager() {
<thead>
<tr>
<th>Имя</th>
<th>Тип</th>
<th>IP адрес</th>
<th>Провайдер</th>
<th>Сервер</th>
@@ -917,18 +961,34 @@ function NetworkConfigManager() {
{filteredGateways.map(gateway => {
const server = getServerInfo(gateway.serverId);
const provider = server?.provider || '';
const country = server?.country || '';
const serverCountry = server?.country || '';
const gatewayCountry = gateway.country || '';
const displayCountry = gatewayCountry || serverCountry;
const gatewayType = GATEWAY_TYPES.find(t => t.value === gateway.type) || GATEWAY_TYPES[0];
const parentGateway = gateway.type === 'recursive' && gateway.parentGatewayId
? getParentGateway(gateway.parentGatewayId)
: null;
return (
<tr key={gateway.id}>
<td>
<div className="d-flex align-items-center">
<span className={`avatar avatar-xs bg-${getProviderColor(provider)}-lt me-2`}>
<IconCircleFilled size={8} className={`text-${getProviderColor(provider)}`} />
<span className={`avatar avatar-xs bg-${gatewayType.color}-lt me-2`}>
<IconCircleFilled size={8} className={`text-${gatewayType.color}`} />
</span>
<strong>{gateway.name || '—'}</strong>
</div>
</td>
<td>
<span className={`badge bg-${gatewayType.color}-lt text-${gatewayType.color}`}>
{gatewayType.label}
</span>
{parentGateway && (
<div className="text-muted small mt-1">
{parentGateway.name}
</div>
)}
</td>
<td>
<code className="d-flex align-items-center gap-1">
{gateway.ip || '—'}
@@ -963,8 +1023,15 @@ function NetworkConfigManager() {
)}
</td>
<td>
{country ? (
<span>{countryToFlag(country)} {country}</span>
{displayCountry ? (
<div>
<span>{countryToFlag(displayCountry)} {displayCountry}</span>
{gatewayCountry && serverCountry && gatewayCountry !== serverCountry && (
<div className="text-muted small" style={{ fontSize: '0.7rem' }}>
сервер: {serverCountry}
</div>
)}
</div>
) : '—'}
</td>
<td className="text-end">
@@ -1215,7 +1282,7 @@ function NetworkConfigManager() {
placeholder="Выберите сервер..."
/>
<div className="form-text">
Страна и провайдер будут автоматически взяты из данных сервера
Провайдер будет автоматически взят из данных сервера
</div>
</div>
<div className="col-md-6">
@@ -1237,6 +1304,49 @@ function NetworkConfigManager() {
placeholder="94.142.140.1"
/>
</div>
<div className="col-md-6">
<FormField
label="Тип gateway"
name="type"
type="select"
value={editingGateway.type}
onChange={(val) => setEditingGateway({ ...editingGateway, type: val, parentGatewayId: val === 'direct' ? '' : editingGateway.parentGatewayId })}
options={GATEWAY_TYPES.map(t => ({ value: t.value, label: `${t.label} - ${t.description}` }))}
required
/>
</div>
<div className="col-md-6">
<FormField
label="Страна"
name="country"
value={editingGateway.country}
onChange={(val) => setEditingGateway({ ...editingGateway, country: val.toUpperCase().slice(0, 2) })}
placeholder="SWE"
/>
<div className="form-text">2-буквенный код страны (ISO 3166-1 alpha-2)</div>
</div>
{editingGateway.type === 'recursive' && (
<div className="col-12">
<label className="form-label">Родительский gateway</label>
<select
className="form-select"
value={editingGateway.parentGatewayId || ''}
onChange={(e) => setEditingGateway({ ...editingGateway, parentGatewayId: e.target.value })}
>
<option value="">Выберите родительский gateway</option>
{config.gateways
.filter(g => g.id !== editingGateway.id && g.type === 'direct')
.map(g => (
<option key={g.id} value={g.id}>
{g.name} {g.ip ? `(${g.ip})` : ''}
</option>
))}
</select>
<div className="form-text">
Рекурсивный gateway ссылается на другой gateway (обычно прямой)
</div>
</div>
)}
<div className="col-12">
<FormField
label="Описание"