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",
|
||||
"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": [
|
||||
|
||||
@@ -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="Описание"
|
||||
|
||||
Reference in New Issue
Block a user