feat(NetworkConfigManager): add gateway templates feature for streamlined gateway creation and configuration, enhancing user experience and efficiency
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m44s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m44s
This commit is contained in:
@@ -65,6 +65,64 @@ const GATEWAY_TYPES = [
|
|||||||
{ value: 'recursive', label: 'Рекурсивный', description: 'Ссылается на другой gateway', color: 'blue' },
|
{ value: 'recursive', label: 'Рекурсивный', description: 'Ссылается на другой gateway', color: 'blue' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Шаблоны стандартных наборов gateways
|
||||||
|
const GATEWAY_TEMPLATES = [
|
||||||
|
{
|
||||||
|
id: 'standard-jumphost',
|
||||||
|
name: 'Стандартный набор для Jumphost',
|
||||||
|
description: 'Direct Gateway + Основной + EU-Шлюзы',
|
||||||
|
gateways: [
|
||||||
|
{
|
||||||
|
type: 'direct',
|
||||||
|
description: 'Direct Gateway',
|
||||||
|
ipTemplate: '{baseIp}.1', // Будет заменено на реальный IP
|
||||||
|
country: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'recursive',
|
||||||
|
description: 'Основной',
|
||||||
|
ipTemplate: '{baseIp}.1',
|
||||||
|
country: '',
|
||||||
|
parentGateways: [], // Будет заполнено пользователем
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'recursive',
|
||||||
|
description: 'EU-Шлюз (VEESP/HIPHOS)',
|
||||||
|
ipTemplate: '{baseIp}.2',
|
||||||
|
country: 'SE',
|
||||||
|
parentGateways: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'recursive',
|
||||||
|
description: 'EU-Шлюз (FIN)',
|
||||||
|
ipTemplate: '{baseIp}.3',
|
||||||
|
country: 'FI',
|
||||||
|
parentGateways: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'minimal-set',
|
||||||
|
name: 'Минимальный набор',
|
||||||
|
description: 'Direct Gateway + Основной',
|
||||||
|
gateways: [
|
||||||
|
{
|
||||||
|
type: 'direct',
|
||||||
|
description: 'Direct Gateway',
|
||||||
|
ipTemplate: '{baseIp}.1',
|
||||||
|
country: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'recursive',
|
||||||
|
description: 'Основной',
|
||||||
|
ipTemplate: '{baseIp}.1',
|
||||||
|
country: '',
|
||||||
|
parentGateways: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
// Пустые объекты
|
// Пустые объекты
|
||||||
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)}`,
|
||||||
@@ -140,6 +198,13 @@ function NetworkConfigManager() {
|
|||||||
const [itemToDelete, setItemToDelete] = useState(null);
|
const [itemToDelete, setItemToDelete] = useState(null);
|
||||||
const [deleteType, setDeleteType] = useState('');
|
const [deleteType, setDeleteType] = useState('');
|
||||||
|
|
||||||
|
// === Gateway Template Modal ===
|
||||||
|
const [gatewayTemplateModalOpen, setGatewayTemplateModalOpen] = useState(false);
|
||||||
|
const [selectedTemplate, setSelectedTemplate] = useState(null);
|
||||||
|
const [templateServerId, setTemplateServerId] = useState('');
|
||||||
|
const [templateBaseIp, setTemplateBaseIp] = useState(''); // Базовый IP для генерации (например, 10.9.9)
|
||||||
|
const [templateGateways, setTemplateGateways] = useState([]); // Массив gateways из шаблона с возможностью редактирования
|
||||||
|
|
||||||
// === Interface Template Modal ===
|
// === Interface Template Modal ===
|
||||||
const [interfaceTemplateModalOpen, setInterfaceTemplateModalOpen] = useState(false);
|
const [interfaceTemplateModalOpen, setInterfaceTemplateModalOpen] = useState(false);
|
||||||
const [templateServer1, setTemplateServer1] = useState('');
|
const [templateServer1, setTemplateServer1] = useState('');
|
||||||
@@ -469,6 +534,151 @@ function NetworkConfigManager() {
|
|||||||
|
|
||||||
const hasActiveFilters = searchTerm || providerFilter || serverFilter || typeFilter || gatewayTypeFilter;
|
const hasActiveFilters = searchTerm || providerFilter || serverFilter || typeFilter || gatewayTypeFilter;
|
||||||
|
|
||||||
|
// === Обработка шаблонов gateways ===
|
||||||
|
const handleOpenGatewayTemplate = () => {
|
||||||
|
setSelectedTemplate(null);
|
||||||
|
setTemplateServerId('');
|
||||||
|
setTemplateBaseIp('');
|
||||||
|
setTemplateGateways([]);
|
||||||
|
setGatewayTemplateModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectTemplate = (template) => {
|
||||||
|
setSelectedTemplate(template);
|
||||||
|
// Инициализируем gateways из шаблона
|
||||||
|
const initializedGateways = template.gateways.map((gw, index) => {
|
||||||
|
// Генерируем IP из шаблона, если есть базовый IP
|
||||||
|
let ip = gw.ipTemplate || '';
|
||||||
|
if (templateBaseIp && gw.ipTemplate) {
|
||||||
|
ip = gw.ipTemplate.replace('{baseIp}', templateBaseIp.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...gw,
|
||||||
|
id: `template-gw-${index}`,
|
||||||
|
ip: ip,
|
||||||
|
description: gw.description || '',
|
||||||
|
country: gw.country || '',
|
||||||
|
parentGateways: gw.parentGateways ? [...gw.parentGateways] : [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
setTemplateGateways(initializedGateways);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Обновление IP адресов при изменении базового IP
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedTemplate && templateBaseIp && templateBaseIp.trim().split('.').length >= 3 && templateGateways.length > 0) {
|
||||||
|
const updatedGateways = templateGateways.map((gw, index) => {
|
||||||
|
const originalTemplate = selectedTemplate.gateways[index];
|
||||||
|
if (originalTemplate && originalTemplate.ipTemplate) {
|
||||||
|
const templateIp = originalTemplate.ipTemplate.replace('{baseIp}', templateBaseIp.trim());
|
||||||
|
// Обновляем IP только если он соответствует шаблону (последний октет совпадает) или пустой
|
||||||
|
const currentIpParts = gw.ip ? gw.ip.split('.') : [];
|
||||||
|
const templateIpParts = templateIp.split('.');
|
||||||
|
|
||||||
|
// Если IP пустой или последний октет совпадает с шаблоном, обновляем
|
||||||
|
if (!gw.ip || (currentIpParts.length === 4 && templateIpParts.length === 4 &&
|
||||||
|
currentIpParts[3] === templateIpParts[3])) {
|
||||||
|
return { ...gw, ip: templateIp };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return gw;
|
||||||
|
});
|
||||||
|
// Обновляем только если есть изменения
|
||||||
|
const hasChanges = updatedGateways.some((gw, i) => gw.ip !== templateGateways[i]?.ip);
|
||||||
|
if (hasChanges) {
|
||||||
|
setTemplateGateways(updatedGateways);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [templateBaseIp, selectedTemplate?.id]);
|
||||||
|
|
||||||
|
const handleCreateGatewaysFromTemplate = () => {
|
||||||
|
if (!selectedTemplate) {
|
||||||
|
notify.error('Выберите шаблон');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!templateServerId) {
|
||||||
|
notify.error('Выберите сервер');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!templateBaseIp || !templateBaseIp.trim()) {
|
||||||
|
notify.error('Укажите базовый IP адрес (например, 10.9.9 или 45.8.248)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем IP адреса из шаблонов
|
||||||
|
const baseIpParts = templateBaseIp.trim().split('.');
|
||||||
|
if (baseIpParts.length < 3) {
|
||||||
|
notify.error('Базовый IP должен содержать минимум 3 октета (например, 10.9.9)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newGateways = [];
|
||||||
|
const usedIps = new Set(config.gateways.map(g => g.ip));
|
||||||
|
|
||||||
|
templateGateways.forEach((gwTemplate, index) => {
|
||||||
|
// Используем IP из редактируемого gateway (уже сгенерированный из шаблона)
|
||||||
|
let ip = gwTemplate.ip || '';
|
||||||
|
|
||||||
|
// Если IP пустой, пытаемся сгенерировать из шаблона
|
||||||
|
if (!ip && selectedTemplate.gateways[index]?.ipTemplate) {
|
||||||
|
ip = selectedTemplate.gateways[index].ipTemplate.replace('{baseIp}', templateBaseIp.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если IP все еще пустой, пропускаем
|
||||||
|
if (!ip || !ip.trim()) {
|
||||||
|
notify.warning(`Gateway ${index + 1}: IP адрес не указан, пропущен`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ip = ip.trim();
|
||||||
|
|
||||||
|
// Если IP уже используется, пропускаем
|
||||||
|
if (usedIps.has(ip)) {
|
||||||
|
notify.warning(`IP ${ip} уже используется, пропущен`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newGateway = {
|
||||||
|
id: `gw-${Date.now()}-${index}-${Math.random().toString(16).slice(2, 6)}`,
|
||||||
|
ip: ip,
|
||||||
|
description: gwTemplate.description || '',
|
||||||
|
serverId: templateServerId,
|
||||||
|
country: gwTemplate.country || '',
|
||||||
|
type: gwTemplate.type,
|
||||||
|
parentGateways: gwTemplate.type === 'recursive'
|
||||||
|
? (gwTemplate.parentGateways || []).filter(p => p && p.id)
|
||||||
|
: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
newGateways.push(newGateway);
|
||||||
|
usedIps.add(ip);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newGateways.length === 0) {
|
||||||
|
notify.error('Не удалось создать ни одного gateway');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем все gateways
|
||||||
|
setConfig(prev => ({
|
||||||
|
...prev,
|
||||||
|
gateways: [...prev.gateways, ...newGateways],
|
||||||
|
}));
|
||||||
|
|
||||||
|
notify.success(`Создано gateways: ${newGateways.length}`);
|
||||||
|
|
||||||
|
// Закрываем модал и сбрасываем значения
|
||||||
|
setGatewayTemplateModalOpen(false);
|
||||||
|
setSelectedTemplate(null);
|
||||||
|
setTemplateServerId('');
|
||||||
|
setTemplateBaseIp('');
|
||||||
|
setTemplateGateways([]);
|
||||||
|
};
|
||||||
|
|
||||||
// === CRUD для Gateways ===
|
// === CRUD для Gateways ===
|
||||||
const handleAddGateway = () => {
|
const handleAddGateway = () => {
|
||||||
setEditingGateway(getEmptyGateway());
|
setEditingGateway(getEmptyGateway());
|
||||||
@@ -2964,10 +3174,20 @@ function NetworkConfigManager() {
|
|||||||
|
|
||||||
{/* Add button */}
|
{/* Add button */}
|
||||||
{activeTab === 'gateways' && (
|
{activeTab === 'gateways' && (
|
||||||
<button className="btn btn-primary btn-sm" onClick={handleAddGateway}>
|
<>
|
||||||
<IconPlus size={16} className="me-1" />
|
<button className="btn btn-primary btn-sm" onClick={handleAddGateway}>
|
||||||
Gateway
|
<IconPlus size={16} className="me-1" />
|
||||||
</button>
|
Gateway
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-primary btn-sm"
|
||||||
|
onClick={handleOpenGatewayTemplate}
|
||||||
|
title="Создать gateways из шаблона"
|
||||||
|
>
|
||||||
|
<IconSparkles size={16} className="me-1" />
|
||||||
|
Шаблон
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{activeTab === 'interfaces' && (
|
{activeTab === 'interfaces' && (
|
||||||
<>
|
<>
|
||||||
@@ -4807,6 +5027,275 @@ function NetworkConfigManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Gateway Template Modal */}
|
||||||
|
<FormModal
|
||||||
|
show={gatewayTemplateModalOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setGatewayTemplateModalOpen(false);
|
||||||
|
setSelectedTemplate(null);
|
||||||
|
setTemplateServerId('');
|
||||||
|
setTemplateBaseIp('');
|
||||||
|
setTemplateGateways([]);
|
||||||
|
}}
|
||||||
|
onSubmit={handleCreateGatewaysFromTemplate}
|
||||||
|
title="Создать gateways из шаблона"
|
||||||
|
submitLabel={selectedTemplate ? `Создать ${templateGateways.length} gateways` : "Выберите шаблон"}
|
||||||
|
submitIcon={IconSparkles}
|
||||||
|
size="lg"
|
||||||
|
submitDisabled={!selectedTemplate || !templateServerId || !templateBaseIp}
|
||||||
|
>
|
||||||
|
<div className="row g-3">
|
||||||
|
{/* Выбор шаблона */}
|
||||||
|
<div className="col-12">
|
||||||
|
<label className="form-label required">Шаблон набора gateways</label>
|
||||||
|
<div className="row g-2">
|
||||||
|
{GATEWAY_TEMPLATES.map(template => {
|
||||||
|
// Генерируем предпросмотр IP адресов если есть базовый IP
|
||||||
|
const previewIps = templateBaseIp && templateBaseIp.trim().split('.').length >= 3
|
||||||
|
? template.gateways.map(gw => {
|
||||||
|
if (gw.ipTemplate) {
|
||||||
|
return gw.ipTemplate.replace('{baseIp}', templateBaseIp.trim());
|
||||||
|
}
|
||||||
|
return '—';
|
||||||
|
})
|
||||||
|
: template.gateways.map(() => '—');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={template.id} className="col-12 col-md-6">
|
||||||
|
<div
|
||||||
|
className={`card cursor-pointer ${selectedTemplate?.id === template.id ? 'border-primary' : ''}`}
|
||||||
|
style={{
|
||||||
|
transition: 'all 0.2s',
|
||||||
|
borderWidth: selectedTemplate?.id === template.id ? '2px' : '1px',
|
||||||
|
backgroundColor: selectedTemplate?.id === template.id ? 'rgba(32, 107, 196, 0.05)' : 'transparent'
|
||||||
|
}}
|
||||||
|
onClick={() => handleSelectTemplate(template)}
|
||||||
|
>
|
||||||
|
<div className="card-body p-3">
|
||||||
|
<div className="d-flex align-items-start">
|
||||||
|
<div className="flex-grow-1">
|
||||||
|
<h5 className="mb-1 fw-bold">{template.name}</h5>
|
||||||
|
<p className="text-muted small mb-2">{template.description}</p>
|
||||||
|
<div className="d-flex align-items-center gap-2 mb-2">
|
||||||
|
<span className="badge bg-primary-lt text-primary">
|
||||||
|
{template.gateways.length} {template.gateways.length === 1 ? 'gateway' : 'gateways'}
|
||||||
|
</span>
|
||||||
|
<span className="text-muted small">
|
||||||
|
{template.gateways.filter(g => g.type === 'direct').length} прямых, {template.gateways.filter(g => g.type === 'recursive').length} рекурсивных
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{templateBaseIp && templateBaseIp.trim().split('.').length >= 3 && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<div className="text-muted small mb-1">Предпросмотр IP:</div>
|
||||||
|
<div className="d-flex flex-wrap gap-1">
|
||||||
|
{previewIps.map((ip, idx) => (
|
||||||
|
<code key={idx} className="small" style={{ fontSize: '0.75rem', backgroundColor: 'rgba(0, 0, 0, 0.05)', padding: '2px 6px', borderRadius: '3px' }}>
|
||||||
|
{ip}
|
||||||
|
</code>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{selectedTemplate?.id === template.id && (
|
||||||
|
<IconCircleFilled size={20} className="text-primary flex-shrink-0" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Общие параметры */}
|
||||||
|
{selectedTemplate && (
|
||||||
|
<>
|
||||||
|
<div className="col-12">
|
||||||
|
<hr />
|
||||||
|
<h5 className="mb-3">Общие параметры</h5>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12">
|
||||||
|
<label className="form-label required">Сервер</label>
|
||||||
|
<ServerAutocompleteInput
|
||||||
|
value={templateServerId}
|
||||||
|
onChange={(val) => setTemplateServerId(val)}
|
||||||
|
servers={servers}
|
||||||
|
placeholder="Выберите сервер для всех gateways..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12">
|
||||||
|
<label className="form-label required">Базовый IP адрес</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
value={templateBaseIp}
|
||||||
|
onChange={(e) => setTemplateBaseIp(e.target.value)}
|
||||||
|
placeholder="10.9.9 или 45.8.248 (минимум 3 октета)"
|
||||||
|
/>
|
||||||
|
<div className="form-text">
|
||||||
|
Базовый IP для генерации адресов gateways. Например, для "10.9.9" будут созданы IP: 10.9.9.1, 10.9.9.2, 10.9.9.3 и т.д.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Редактирование отдельных gateways */}
|
||||||
|
<div className="col-12">
|
||||||
|
<hr />
|
||||||
|
<h5 className="mb-3">Настройка gateways</h5>
|
||||||
|
<div className="d-flex flex-column gap-3">
|
||||||
|
{templateGateways.map((gw, index) => {
|
||||||
|
const gatewayType = GATEWAY_TYPES.find(t => t.value === gw.type);
|
||||||
|
return (
|
||||||
|
<div key={gw.id || index} className="card border">
|
||||||
|
<div className="card-header py-2" style={{ backgroundColor: 'rgba(0, 0, 0, 0.02)' }}>
|
||||||
|
<div className="d-flex align-items-center justify-content-between">
|
||||||
|
<div className="d-flex align-items-center gap-2">
|
||||||
|
<span className={`badge bg-${gatewayType?.color || 'secondary'}-lt text-${gatewayType?.color || 'secondary'}`}>
|
||||||
|
{gatewayType?.label || gw.type}
|
||||||
|
</span>
|
||||||
|
<strong>Gateway {index + 1}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card-body p-3">
|
||||||
|
<div className="row g-2">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label className="form-label small">Название</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
value={gw.description || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newGateways = [...templateGateways];
|
||||||
|
newGateways[index].description = e.target.value;
|
||||||
|
setTemplateGateways(newGateways);
|
||||||
|
}}
|
||||||
|
placeholder="Название gateway"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label className="form-label small">IP адрес</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
value={gw.ip || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newGateways = [...templateGateways];
|
||||||
|
newGateways[index].ip = e.target.value;
|
||||||
|
setTemplateGateways(newGateways);
|
||||||
|
}}
|
||||||
|
placeholder={gw.ipTemplate ? gw.ipTemplate.replace('{baseIp}', templateBaseIp || 'X.X.X') : 'IP адрес'}
|
||||||
|
/>
|
||||||
|
<div className="form-text small">
|
||||||
|
Шаблон: {gw.ipTemplate || '—'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label className="form-label small">Страна</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
value={gw.country || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newGateways = [...templateGateways];
|
||||||
|
newGateways[index].country = e.target.value.toUpperCase().slice(0, 2);
|
||||||
|
setTemplateGateways(newGateways);
|
||||||
|
}}
|
||||||
|
placeholder="SE, FI, RU..."
|
||||||
|
maxLength={2}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{gw.type === 'recursive' && (
|
||||||
|
<div className="col-12">
|
||||||
|
<label className="form-label small">Родительские gateway/интерфейсы</label>
|
||||||
|
<div className="d-flex flex-column gap-2">
|
||||||
|
{(gw.parentGateways || []).map((parent, parentIndex) => {
|
||||||
|
const parentInfo = getParentGateway(parent.id);
|
||||||
|
return (
|
||||||
|
<div key={parentIndex} className="d-flex align-items-center gap-2">
|
||||||
|
<div className="flex-grow-1">
|
||||||
|
<GatewayAutocompleteInput
|
||||||
|
value={parent.id}
|
||||||
|
onChange={(val) => {
|
||||||
|
const newGateways = [...templateGateways];
|
||||||
|
if (!newGateways[index].parentGateways) {
|
||||||
|
newGateways[index].parentGateways = [];
|
||||||
|
}
|
||||||
|
newGateways[index].parentGateways[parentIndex] = { ...newGateways[index].parentGateways[parentIndex], id: val };
|
||||||
|
setTemplateGateways(newGateways);
|
||||||
|
}}
|
||||||
|
gateways={config.gateways}
|
||||||
|
interfaces={config.tunnelInterfaces}
|
||||||
|
placeholder="Выберите родительский gateway или интерфейс..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style={{ width: '100px' }}>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
value={parent.distance || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newGateways = [...templateGateways];
|
||||||
|
if (!newGateways[index].parentGateways) {
|
||||||
|
newGateways[index].parentGateways = [];
|
||||||
|
}
|
||||||
|
newGateways[index].parentGateways[parentIndex] = {
|
||||||
|
...newGateways[index].parentGateways[parentIndex],
|
||||||
|
distance: e.target.value ? parseInt(e.target.value) : undefined
|
||||||
|
};
|
||||||
|
setTemplateGateways(newGateways);
|
||||||
|
}}
|
||||||
|
placeholder="Distance"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-danger btn-icon btn-sm"
|
||||||
|
onClick={() => {
|
||||||
|
const newGateways = [...templateGateways];
|
||||||
|
newGateways[index].parentGateways = (newGateways[index].parentGateways || []).filter((_, i) => i !== parentIndex);
|
||||||
|
setTemplateGateways(newGateways);
|
||||||
|
}}
|
||||||
|
title="Удалить"
|
||||||
|
>
|
||||||
|
<IconTrash size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-primary btn-sm"
|
||||||
|
onClick={() => {
|
||||||
|
const newGateways = [...templateGateways];
|
||||||
|
if (!newGateways[index].parentGateways) {
|
||||||
|
newGateways[index].parentGateways = [];
|
||||||
|
}
|
||||||
|
newGateways[index].parentGateways.push({ id: '', distance: undefined });
|
||||||
|
setTemplateGateways(newGateways);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconPlus size={16} className="me-1" />
|
||||||
|
Добавить родительский gateway
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FormModal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user