feat(NetworkConfigManager, FilterManager): enhance gateway display and validation; update UI to show comments and improve IP handling in gateway configuration
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m43s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m43s
This commit is contained in:
@@ -482,12 +482,12 @@ function GatewayAutocomplete({ label, value, onChange, gateways = [], required =
|
|||||||
</span>
|
</span>
|
||||||
<div className="flex-grow-1 min-w-0">
|
<div className="flex-grow-1 min-w-0">
|
||||||
<div className="d-flex align-items-center gap-2">
|
<div className="d-flex align-items-center gap-2">
|
||||||
<span className="fw-bold">{gw.name}</span>
|
<code className="fw-bold">{gw.name}</code>
|
||||||
{gw.ip && <span className="text-muted">({gw.ip})</span>}
|
{gw.comment && <span className="text-muted">({gw.comment})</span>}
|
||||||
{gw.serverDns && <span className="text-muted">— {gw.serverDns}</span>}
|
{gw.serverDns && <span className="text-muted">— {gw.serverDns}</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted small">
|
<div className="text-muted small">
|
||||||
{[gw.country, gw.comment].filter(Boolean).join(' · ') || 'Шлюз'}
|
{[gw.country].filter(Boolean).join(' · ') || 'Шлюз'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{gw.primary && <span className="badge bg-green-lt text-green">Основной</span>}
|
{gw.primary && <span className="badge bg-green-lt text-green">Основной</span>}
|
||||||
@@ -508,9 +508,12 @@ function GatewayAutocomplete({ label, value, onChange, gateways = [], required =
|
|||||||
{selectedGateway.country ? countryToFlag(selectedGateway.country) : <IconServer size={14} />}
|
{selectedGateway.country ? countryToFlag(selectedGateway.country) : <IconServer size={14} />}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex-grow-1">
|
<div className="flex-grow-1">
|
||||||
<div className="fw-bold">{selectedGateway.name}</div>
|
<div className="fw-bold">
|
||||||
|
<code>{selectedGateway.name}</code>
|
||||||
|
{selectedGateway.comment && <span className="text-muted ms-2">({selectedGateway.comment})</span>}
|
||||||
|
</div>
|
||||||
<div className="text-muted small">
|
<div className="text-muted small">
|
||||||
{[selectedGateway.ip, selectedGateway.serverDns, selectedGateway.comment].filter(Boolean).join(' · ')}
|
{[selectedGateway.serverDns].filter(Boolean).join(' · ')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{selectedGateway.primary && <span className="badge bg-green-lt text-green">Основной</span>}
|
{selectedGateway.primary && <span className="badge bg-green-lt text-green">Основной</span>}
|
||||||
|
|||||||
@@ -329,13 +329,39 @@ function NetworkConfigManager() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveGateway = (gatewayData) => {
|
const handleSaveGateway = (gatewayData) => {
|
||||||
|
// Валидация: IP адрес обязателен
|
||||||
|
if (!gatewayData.ip || !gatewayData.ip.trim()) {
|
||||||
|
notify.error('IP адрес обязателен');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (gatewayModalMode === 'add') {
|
if (gatewayModalMode === 'add') {
|
||||||
|
// Проверка на дубликат IP
|
||||||
|
const existingGateway = config.gateways.find(g => g.ip === gatewayData.ip.trim());
|
||||||
|
if (existingGateway) {
|
||||||
|
notify.error(`Gateway с IP ${gatewayData.ip} уже существует`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
setConfig(prev => ({
|
setConfig(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
gateways: [...prev.gateways, gatewayData],
|
gateways: [...prev.gateways, gatewayData],
|
||||||
}));
|
}));
|
||||||
notify.success('Gateway добавлен');
|
notify.success('Gateway добавлен');
|
||||||
} else {
|
} else {
|
||||||
|
// При редактировании ищем по ID, но проверяем, не занят ли новый IP другим gateway
|
||||||
|
const currentGateway = config.gateways.find(g => g.id === gatewayData.id);
|
||||||
|
if (!currentGateway) {
|
||||||
|
notify.error('Gateway не найден');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если IP изменился, проверяем, не занят ли он другим gateway
|
||||||
|
if (gatewayData.ip.trim() !== currentGateway.ip &&
|
||||||
|
config.gateways.some(g => g.id !== gatewayData.id && g.ip === gatewayData.ip.trim())) {
|
||||||
|
notify.error(`Gateway с IP ${gatewayData.ip} уже существует`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setConfig(prev => ({
|
setConfig(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
gateways: prev.gateways.map(g => g.id === gatewayData.id ? gatewayData : g),
|
gateways: prev.gateways.map(g => g.id === gatewayData.id ? gatewayData : g),
|
||||||
@@ -701,7 +727,7 @@ function NetworkConfigManager() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-muted small">
|
<div className="text-muted small">
|
||||||
{provider || '—'}
|
{gateway.description || provider || '—'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="dropdown">
|
<div className="dropdown">
|
||||||
@@ -756,11 +782,6 @@ function NetworkConfigManager() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{gateway.description && (
|
|
||||||
<div className="text-muted small mt-1 text-truncate" title={gateway.description}>
|
|
||||||
{gateway.description}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1096,9 +1117,8 @@ function NetworkConfigManager() {
|
|||||||
<table className="table card-table table-vcenter">
|
<table className="table card-table table-vcenter">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Имя</th>
|
|
||||||
<th>Тип</th>
|
|
||||||
<th>IP адрес</th>
|
<th>IP адрес</th>
|
||||||
|
<th>Тип</th>
|
||||||
<th>Провайдер</th>
|
<th>Провайдер</th>
|
||||||
<th>Сервер</th>
|
<th>Сервер</th>
|
||||||
<th>Страна</th>
|
<th>Страна</th>
|
||||||
@@ -1124,7 +1144,24 @@ function NetworkConfigManager() {
|
|||||||
<span className={`avatar avatar-xs bg-${gatewayType.color}-lt me-2`}>
|
<span className={`avatar avatar-xs bg-${gatewayType.color}-lt me-2`}>
|
||||||
<IconCircleFilled size={8} className={`text-${gatewayType.color}`} />
|
<IconCircleFilled size={8} className={`text-${gatewayType.color}`} />
|
||||||
</span>
|
</span>
|
||||||
<code className="fw-medium">{gateway.ip || '—'}</code>
|
<div className="flex-fill">
|
||||||
|
<div className="d-flex align-items-center gap-1">
|
||||||
|
<code className="fw-medium">{gateway.ip || '—'}</code>
|
||||||
|
{gateway.ip && (
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost-secondary btn-icon p-0"
|
||||||
|
onClick={() => copyToClipboard(gateway.ip)}
|
||||||
|
style={{ width: 20, height: 20 }}
|
||||||
|
title="Копировать IP"
|
||||||
|
>
|
||||||
|
<IconCopy size={12} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{gateway.description && (
|
||||||
|
<div className="text-muted small">{gateway.description}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -1140,20 +1177,6 @@ function NetworkConfigManager() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
|
||||||
<code className="d-flex align-items-center gap-1">
|
|
||||||
{gateway.ip || '—'}
|
|
||||||
{gateway.ip && (
|
|
||||||
<button
|
|
||||||
className="btn btn-ghost-secondary btn-icon p-0"
|
|
||||||
onClick={() => copyToClipboard(gateway.ip)}
|
|
||||||
style={{ width: 20, height: 20 }}
|
|
||||||
>
|
|
||||||
<IconCopy size={12} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</code>
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
{provider ? (
|
{provider ? (
|
||||||
<span className={`badge bg-${getProviderColor(provider)}-lt text-${getProviderColor(provider)}`}>
|
<span className={`badge bg-${getProviderColor(provider)}-lt text-${getProviderColor(provider)}`}>
|
||||||
@@ -1442,22 +1465,22 @@ function NetworkConfigManager() {
|
|||||||
onChange={(val) => setEditingGateway({ ...editingGateway, ip: val })}
|
onChange={(val) => setEditingGateway({ ...editingGateway, ip: val })}
|
||||||
placeholder="94.142.140.1"
|
placeholder="94.142.140.1"
|
||||||
required
|
required
|
||||||
disabled={gatewayModalMode === 'edit'}
|
|
||||||
/>
|
/>
|
||||||
{gatewayModalMode === 'edit' && (
|
<div className="form-text">
|
||||||
<div className="form-text text-muted">
|
IP адрес является главным идентификатором gateway
|
||||||
IP адрес нельзя изменить после создания
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<FormField
|
<FormField
|
||||||
label="Комментарий"
|
label="Название (комментарий)"
|
||||||
name="description"
|
name="description"
|
||||||
value={editingGateway.description}
|
value={editingGateway.description}
|
||||||
onChange={(val) => setEditingGateway({ ...editingGateway, description: val })}
|
onChange={(val) => setEditingGateway({ ...editingGateway, description: val })}
|
||||||
placeholder="Основной выход через Cloudflare"
|
placeholder="SWE-IHOR или Основной выход через Cloudflare"
|
||||||
/>
|
/>
|
||||||
|
<div className="form-text">
|
||||||
|
Название gateway для удобства (отображается в фильтрах)
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
<FormField
|
<FormField
|
||||||
@@ -1496,7 +1519,7 @@ function NetworkConfigManager() {
|
|||||||
.filter(g => g.id !== editingGateway.id && g.type === 'direct' && g.ip)
|
.filter(g => g.id !== editingGateway.id && g.type === 'direct' && g.ip)
|
||||||
.map(g => (
|
.map(g => (
|
||||||
<option key={g.id} value={g.id}>
|
<option key={g.id} value={g.id}>
|
||||||
Gateway: {g.ip} {g.description ? `(${g.description})` : ''}
|
Gateway: {g.ip} {g.description ? `- ${g.description}` : ''}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</optgroup>
|
</optgroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user