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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m52s
This commit is contained in:
@@ -5,35 +5,55 @@
|
|||||||
"name": "SWE-IHOR",
|
"name": "SWE-IHOR",
|
||||||
"ip": "94.142.140.1",
|
"ip": "94.142.140.1",
|
||||||
"description": "Cloudflare Sweden - основной выход",
|
"description": "Cloudflare Sweden - основной выход",
|
||||||
"serverId": "SWE-HIPHOST"
|
"serverId": "SWE-HIPHOST",
|
||||||
|
"country": "SWE",
|
||||||
|
"type": "direct"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "gw-bunny-swe",
|
"id": "gw-bunny-swe",
|
||||||
"name": "SWE-BUNNY",
|
"name": "SWE-BUNNY",
|
||||||
"ip": "94.142.140.2",
|
"ip": "94.142.140.2",
|
||||||
"description": "Bunny CDN Sweden",
|
"description": "Bunny CDN Sweden",
|
||||||
"serverId": "SWE-HIPHOST"
|
"serverId": "SWE-HIPHOST",
|
||||||
|
"country": "SWE",
|
||||||
|
"type": "direct"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"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",
|
||||||
"description": "Telegram DC2",
|
"description": "Telegram DC2",
|
||||||
"serverId": "NL-AMSTERDAM"
|
"serverId": "NL-AMSTERDAM",
|
||||||
|
"country": "NL",
|
||||||
|
"type": "direct"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "gw-hetzner-de",
|
"id": "gw-hetzner-de",
|
||||||
"name": "DE-HETZNER",
|
"name": "DE-HETZNER",
|
||||||
"ip": "195.100.30.21",
|
"ip": "195.100.30.21",
|
||||||
"description": "Hetzner Frankfurt",
|
"description": "Hetzner Frankfurt",
|
||||||
"serverId": "DE-FRANKFURT"
|
"serverId": "DE-FRANKFURT",
|
||||||
|
"country": "DE",
|
||||||
|
"type": "direct"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "gw-fastly-us",
|
"id": "gw-fastly-us",
|
||||||
"name": "US-FASTLY",
|
"name": "US-FASTLY",
|
||||||
"ip": "151.101.1.1",
|
"ip": "151.101.1.1",
|
||||||
"description": "Fastly US East",
|
"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": [
|
"tunnelInterfaces": [
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ const INTERFACE_TYPES = [
|
|||||||
{ value: 'OpenVPN', label: 'OpenVPN', color: 'orange' },
|
{ 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 = () => ({
|
const getEmptyGateway = () => ({
|
||||||
id: `gw-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`,
|
id: `gw-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`,
|
||||||
@@ -57,6 +63,9 @@ const getEmptyGateway = () => ({
|
|||||||
ip: '',
|
ip: '',
|
||||||
description: '',
|
description: '',
|
||||||
serverId: '',
|
serverId: '',
|
||||||
|
country: '',
|
||||||
|
type: 'direct',
|
||||||
|
parentGatewayId: '', // Для рекурсивных - ID родительского gateway
|
||||||
});
|
});
|
||||||
|
|
||||||
const getEmptyInterface = () => ({
|
const getEmptyInterface = () => ({
|
||||||
@@ -97,6 +106,7 @@ function NetworkConfigManager() {
|
|||||||
const [providerFilter, setProviderFilter] = useState('');
|
const [providerFilter, setProviderFilter] = useState('');
|
||||||
const [serverFilter, setServerFilter] = useState('');
|
const [serverFilter, setServerFilter] = useState('');
|
||||||
const [typeFilter, setTypeFilter] = useState('');
|
const [typeFilter, setTypeFilter] = useState('');
|
||||||
|
const [gatewayTypeFilter, setGatewayTypeFilter] = useState(''); // Для фильтрации gateway по типу
|
||||||
const [viewMode, setViewMode] = useState('cards'); // 'cards' | 'table'
|
const [viewMode, setViewMode] = useState('cards'); // 'cards' | 'table'
|
||||||
|
|
||||||
// === Modals ===
|
// === Modals ===
|
||||||
@@ -207,6 +217,10 @@ function NetworkConfigManager() {
|
|||||||
result = result.filter(g => g.serverId === serverFilter);
|
result = result.filter(g => g.serverId === serverFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (gatewayTypeFilter) {
|
||||||
|
result = result.filter(g => g.type === gatewayTypeFilter);
|
||||||
|
}
|
||||||
|
|
||||||
if (searchTerm) {
|
if (searchTerm) {
|
||||||
const term = searchTerm.toLowerCase();
|
const term = searchTerm.toLowerCase();
|
||||||
result = result.filter(g => {
|
result = result.filter(g => {
|
||||||
@@ -214,6 +228,7 @@ function NetworkConfigManager() {
|
|||||||
return (
|
return (
|
||||||
g.name?.toLowerCase().includes(term) ||
|
g.name?.toLowerCase().includes(term) ||
|
||||||
g.ip?.toLowerCase().includes(term) ||
|
g.ip?.toLowerCase().includes(term) ||
|
||||||
|
g.country?.toLowerCase().includes(term) ||
|
||||||
server?.country?.toLowerCase().includes(term) ||
|
server?.country?.toLowerCase().includes(term) ||
|
||||||
server?.provider?.toLowerCase().includes(term) ||
|
server?.provider?.toLowerCase().includes(term) ||
|
||||||
g.description?.toLowerCase().includes(term) ||
|
g.description?.toLowerCase().includes(term) ||
|
||||||
@@ -223,7 +238,7 @@ function NetworkConfigManager() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, [config.gateways, providerFilter, serverFilter, searchTerm, servers]);
|
}, [config.gateways, providerFilter, serverFilter, typeFilter, searchTerm, servers]);
|
||||||
|
|
||||||
const filteredInterfaces = useMemo(() => {
|
const filteredInterfaces = useMemo(() => {
|
||||||
let result = [...(config.tunnelInterfaces || [])];
|
let result = [...(config.tunnelInterfaces || [])];
|
||||||
@@ -292,9 +307,10 @@ function NetworkConfigManager() {
|
|||||||
setProviderFilter('');
|
setProviderFilter('');
|
||||||
setServerFilter('');
|
setServerFilter('');
|
||||||
setTypeFilter('');
|
setTypeFilter('');
|
||||||
|
setGatewayTypeFilter('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasActiveFilters = searchTerm || providerFilter || serverFilter || typeFilter;
|
const hasActiveFilters = searchTerm || providerFilter || serverFilter || typeFilter || gatewayTypeFilter;
|
||||||
|
|
||||||
// === CRUD для Gateways ===
|
// === CRUD для Gateways ===
|
||||||
const handleAddGateway = () => {
|
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 ===
|
// === Рендер карточки Gateway ===
|
||||||
const renderGatewayCard = (gateway) => {
|
const renderGatewayCard = (gateway) => {
|
||||||
const server = getServerInfo(gateway.serverId);
|
const server = getServerInfo(gateway.serverId);
|
||||||
const provider = server?.provider || '';
|
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 (
|
return (
|
||||||
<div key={gateway.id} className="col-sm-6 col-lg-4">
|
<div key={gateway.id} className="col-sm-6 col-lg-4">
|
||||||
<div className="card card-sm">
|
<div className="card card-sm">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<div className="d-flex align-items-center mb-2">
|
<div className="d-flex align-items-center mb-2">
|
||||||
<span className={`avatar avatar-sm bg-${getProviderColor(provider)}-lt me-2`}>
|
<span className={`avatar avatar-sm bg-${gatewayType.color}-lt me-2`}>
|
||||||
<IconWorld size={16} className={`text-${getProviderColor(provider)}`} />
|
<IconWorld size={16} className={`text-${gatewayType.color}`} />
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-fill">
|
<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">
|
<div className="text-muted small">
|
||||||
{provider || '—'}
|
{provider || '—'}
|
||||||
</div>
|
</div>
|
||||||
@@ -567,10 +600,20 @@ function NetworkConfigManager() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{country && (
|
{displayCountry && (
|
||||||
<div className="d-flex align-items-center gap-1 text-muted small">
|
<div className="d-flex align-items-center gap-1 text-muted small mb-1">
|
||||||
<span>{countryToFlag(country)}</span>
|
<span>{countryToFlag(displayCountry)}</span>
|
||||||
<span>{country}</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -906,6 +949,7 @@ function NetworkConfigManager() {
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Имя</th>
|
<th>Имя</th>
|
||||||
|
<th>Тип</th>
|
||||||
<th>IP адрес</th>
|
<th>IP адрес</th>
|
||||||
<th>Провайдер</th>
|
<th>Провайдер</th>
|
||||||
<th>Сервер</th>
|
<th>Сервер</th>
|
||||||
@@ -917,18 +961,34 @@ function NetworkConfigManager() {
|
|||||||
{filteredGateways.map(gateway => {
|
{filteredGateways.map(gateway => {
|
||||||
const server = getServerInfo(gateway.serverId);
|
const server = getServerInfo(gateway.serverId);
|
||||||
const provider = server?.provider || '';
|
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 (
|
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(provider)}-lt me-2`}>
|
<span className={`avatar avatar-xs bg-${gatewayType.color}-lt me-2`}>
|
||||||
<IconCircleFilled size={8} className={`text-${getProviderColor(provider)}`} />
|
<IconCircleFilled size={8} className={`text-${gatewayType.color}`} />
|
||||||
</span>
|
</span>
|
||||||
<strong>{gateway.name || '—'}</strong>
|
<strong>{gateway.name || '—'}</strong>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</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>
|
<td>
|
||||||
<code className="d-flex align-items-center gap-1">
|
<code className="d-flex align-items-center gap-1">
|
||||||
{gateway.ip || '—'}
|
{gateway.ip || '—'}
|
||||||
@@ -963,8 +1023,15 @@ function NetworkConfigManager() {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{country ? (
|
{displayCountry ? (
|
||||||
<span>{countryToFlag(country)} {country}</span>
|
<div>
|
||||||
|
<span>{countryToFlag(displayCountry)} {displayCountry}</span>
|
||||||
|
{gatewayCountry && serverCountry && gatewayCountry !== serverCountry && (
|
||||||
|
<div className="text-muted small" style={{ fontSize: '0.7rem' }}>
|
||||||
|
сервер: {serverCountry}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : '—'}
|
) : '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-end">
|
<td className="text-end">
|
||||||
@@ -1215,7 +1282,7 @@ function NetworkConfigManager() {
|
|||||||
placeholder="Выберите сервер..."
|
placeholder="Выберите сервер..."
|
||||||
/>
|
/>
|
||||||
<div className="form-text">
|
<div className="form-text">
|
||||||
Страна и провайдер будут автоматически взяты из данных сервера
|
Провайдер будет автоматически взят из данных сервера
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
@@ -1237,6 +1304,49 @@ function NetworkConfigManager() {
|
|||||||
placeholder="94.142.140.1"
|
placeholder="94.142.140.1"
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div className="col-12">
|
||||||
<FormField
|
<FormField
|
||||||
label="Описание"
|
label="Описание"
|
||||||
|
|||||||
Reference in New Issue
Block a user