feat(NetworkConfigManager): add interface template creation feature; implement modal for generating interfaces from server templates with conflict checking and preview functionality
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m36s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m36s
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
|||||||
IconSearch,
|
IconSearch,
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
IconWand,
|
IconWand,
|
||||||
|
IconSparkles,
|
||||||
IconFilter,
|
IconFilter,
|
||||||
IconX,
|
IconX,
|
||||||
IconCircleFilled,
|
IconCircleFilled,
|
||||||
@@ -131,6 +132,13 @@ function NetworkConfigManager() {
|
|||||||
const [itemToDelete, setItemToDelete] = useState(null);
|
const [itemToDelete, setItemToDelete] = useState(null);
|
||||||
const [deleteType, setDeleteType] = useState('');
|
const [deleteType, setDeleteType] = useState('');
|
||||||
|
|
||||||
|
// === Interface Template Modal ===
|
||||||
|
const [interfaceTemplateModalOpen, setInterfaceTemplateModalOpen] = useState(false);
|
||||||
|
const [templateServer1, setTemplateServer1] = useState('');
|
||||||
|
const [templateServer2, setTemplateServer2] = useState('');
|
||||||
|
const [templateType, setTemplateType] = useState('GRE');
|
||||||
|
const [templateNamePrefix, setTemplateNamePrefix] = useState('');
|
||||||
|
|
||||||
// === MikroTik Code Generation ===
|
// === MikroTik Code Generation ===
|
||||||
const [mikrotikCodeModalOpen, setMikrotikCodeModalOpen] = useState(false);
|
const [mikrotikCodeModalOpen, setMikrotikCodeModalOpen] = useState(false);
|
||||||
const [generatedMikrotikCode, setGeneratedMikrotikCode] = useState([]);
|
const [generatedMikrotikCode, setGeneratedMikrotikCode] = useState([]);
|
||||||
@@ -432,6 +440,86 @@ function NetworkConfigManager() {
|
|||||||
setDeleteModalOpen(true);
|
setDeleteModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// === Генерация имени интерфейса на основе серверов ===
|
||||||
|
const generateInterfaceName = (server1, server2, type, prefix = '') => {
|
||||||
|
const server1Name = server1?.dns?.split('.')[0] || server1?.ip?.split('.').join('-') || 'server1';
|
||||||
|
const server2Name = server2?.dns?.split('.')[0] || server2?.ip?.split('.').join('-') || 'server2';
|
||||||
|
const typeLower = type.toLowerCase();
|
||||||
|
const prefixPart = prefix ? `${prefix}-` : '';
|
||||||
|
return `${prefixPart}${typeLower}-${server1Name}-${server2Name}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// === Создание интерфейса из шаблона ===
|
||||||
|
const handleCreateInterfaceFromTemplate = () => {
|
||||||
|
if (!templateServer1 || !templateServer2) {
|
||||||
|
notify.error('Выберите оба сервера');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (templateServer1 === templateServer2) {
|
||||||
|
notify.error('Серверы должны быть разными');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const server1 = getServerInfo(templateServer1);
|
||||||
|
const server2 = getServerInfo(templateServer2);
|
||||||
|
|
||||||
|
if (!server1 || !server2) {
|
||||||
|
notify.error('Не удалось найти информацию о серверах');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем имена интерфейсов
|
||||||
|
const name1 = generateInterfaceName(server1, server2, templateType, templateNamePrefix);
|
||||||
|
const name2 = generateInterfaceName(server2, server1, templateType, templateNamePrefix);
|
||||||
|
|
||||||
|
// Генерируем IP адреса
|
||||||
|
const localIp = generateFreePrivateIp(null, false, null);
|
||||||
|
if (!localIp) {
|
||||||
|
notify.error('Не удалось найти свободный Local IP адрес');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remoteIp = generateFreePrivateIp(null, true, localIp);
|
||||||
|
if (!remoteIp) {
|
||||||
|
notify.error('Не удалось найти свободный Remote IP адрес');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем конфликты
|
||||||
|
const newInterface = {
|
||||||
|
...getEmptyInterface(),
|
||||||
|
name: name1,
|
||||||
|
name2: name2,
|
||||||
|
type: templateType,
|
||||||
|
localIp: localIp,
|
||||||
|
remoteIp: remoteIp,
|
||||||
|
serverId: templateServer1,
|
||||||
|
serverId2: templateServer2
|
||||||
|
};
|
||||||
|
|
||||||
|
const conflicts = checkInterfaceIpConflict(newInterface);
|
||||||
|
if (conflicts.length > 0) {
|
||||||
|
notify.error('Обнаружены конфликты IP адресов. Попробуйте еще раз.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем интерфейс
|
||||||
|
setConfig(prev => ({
|
||||||
|
...prev,
|
||||||
|
tunnelInterfaces: [...prev.tunnelInterfaces, newInterface],
|
||||||
|
}));
|
||||||
|
|
||||||
|
notify.success(`Интерфейс создан: ${name1} (${localIp} ↔ ${remoteIp})`);
|
||||||
|
|
||||||
|
// Закрываем модал и сбрасываем значения
|
||||||
|
setInterfaceTemplateModalOpen(false);
|
||||||
|
setTemplateServer1('');
|
||||||
|
setTemplateServer2('');
|
||||||
|
setTemplateType('GRE');
|
||||||
|
setTemplateNamePrefix('');
|
||||||
|
};
|
||||||
|
|
||||||
// === CRUD для Interfaces ===
|
// === CRUD для Interfaces ===
|
||||||
const handleAddInterface = () => {
|
const handleAddInterface = () => {
|
||||||
setEditingInterface(getEmptyInterface());
|
setEditingInterface(getEmptyInterface());
|
||||||
@@ -1771,10 +1859,20 @@ function NetworkConfigManager() {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{activeTab === 'interfaces' && (
|
{activeTab === 'interfaces' && (
|
||||||
<button className="btn btn-primary btn-sm" onClick={handleAddInterface}>
|
<>
|
||||||
<IconPlus size={16} className="me-1" />
|
<button className="btn btn-primary btn-sm" onClick={handleAddInterface}>
|
||||||
Интерфейс
|
<IconPlus size={16} className="me-1" />
|
||||||
</button>
|
Интерфейс
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-primary btn-sm"
|
||||||
|
onClick={() => setInterfaceTemplateModalOpen(true)}
|
||||||
|
title="Создать интерфейс из шаблона"
|
||||||
|
>
|
||||||
|
<IconSparkles size={16} className="me-1" />
|
||||||
|
Шаблон
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{activeTab === 'pools' && (
|
{activeTab === 'pools' && (
|
||||||
<button className="btn btn-primary btn-sm" onClick={handleAddPool}>
|
<button className="btn btn-primary btn-sm" onClick={handleAddPool}>
|
||||||
@@ -2642,6 +2740,122 @@ function NetworkConfigManager() {
|
|||||||
)}
|
)}
|
||||||
</FormModal>
|
</FormModal>
|
||||||
|
|
||||||
|
{/* Interface Template Modal */}
|
||||||
|
<FormModal
|
||||||
|
show={interfaceTemplateModalOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setInterfaceTemplateModalOpen(false);
|
||||||
|
setTemplateServer1('');
|
||||||
|
setTemplateServer2('');
|
||||||
|
setTemplateType('GRE');
|
||||||
|
setTemplateNamePrefix('');
|
||||||
|
}}
|
||||||
|
onSubmit={handleCreateInterfaceFromTemplate}
|
||||||
|
title="Создать интерфейс из шаблона"
|
||||||
|
submitLabel="Создать интерфейс"
|
||||||
|
submitIcon={IconSparkles}
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<div className="row g-3">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label className="form-label required">Тип интерфейса</label>
|
||||||
|
<FormField
|
||||||
|
name="type"
|
||||||
|
type="select"
|
||||||
|
value={templateType}
|
||||||
|
onChange={(val) => setTemplateType(val)}
|
||||||
|
options={INTERFACE_TYPES}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label className="form-label">Префикс имени (опционально)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
value={templateNamePrefix}
|
||||||
|
onChange={(e) => setTemplateNamePrefix(e.target.value)}
|
||||||
|
placeholder="tunnel"
|
||||||
|
/>
|
||||||
|
<div className="form-text">Будет добавлен перед именем интерфейса</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label className="form-label required">Сервер 1</label>
|
||||||
|
<ServerAutocompleteInput
|
||||||
|
value={templateServer1}
|
||||||
|
onChange={(val) => setTemplateServer1(val)}
|
||||||
|
servers={servers.filter(s => s.id !== templateServer2 && s.ip !== templateServer2)}
|
||||||
|
placeholder="Выберите первый сервер..."
|
||||||
|
/>
|
||||||
|
{templateServer1 && (() => {
|
||||||
|
const server1 = getServerInfo(templateServer1);
|
||||||
|
return server1 ? (
|
||||||
|
<div className="form-text">
|
||||||
|
{server1.dns || server1.ip}
|
||||||
|
{server1.country && ` • ${countryToFlag(server1.country)} ${server1.country}`}
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label className="form-label required">Сервер 2</label>
|
||||||
|
<ServerAutocompleteInput
|
||||||
|
value={templateServer2}
|
||||||
|
onChange={(val) => setTemplateServer2(val)}
|
||||||
|
servers={servers.filter(s => s.id !== templateServer1 && s.ip !== templateServer1)}
|
||||||
|
placeholder="Выберите второй сервер..."
|
||||||
|
/>
|
||||||
|
{templateServer2 && (() => {
|
||||||
|
const server2 = getServerInfo(templateServer2);
|
||||||
|
return server2 ? (
|
||||||
|
<div className="form-text">
|
||||||
|
{server2.dns || server2.ip}
|
||||||
|
{server2.country && ` • ${countryToFlag(server2.country)} ${server2.country}`}
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
{templateServer1 && templateServer2 && templateServer1 !== templateServer2 && (() => {
|
||||||
|
const server1 = getServerInfo(templateServer1);
|
||||||
|
const server2 = getServerInfo(templateServer2);
|
||||||
|
if (!server1 || !server2) return null;
|
||||||
|
|
||||||
|
const previewName1 = generateInterfaceName(server1, server2, templateType, templateNamePrefix);
|
||||||
|
const previewName2 = generateInterfaceName(server2, server1, templateType, templateNamePrefix);
|
||||||
|
const previewLocalIp = generateFreePrivateIp(null, false, null);
|
||||||
|
const previewRemoteIp = previewLocalIp ? generateFreePrivateIp(null, true, previewLocalIp) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="col-12">
|
||||||
|
<div className="card card-sm border">
|
||||||
|
<div className="card-body">
|
||||||
|
<h6 className="card-title mb-3">Предпросмотр интерфейса:</h6>
|
||||||
|
<div className="row g-2">
|
||||||
|
<div className="col-md-6">
|
||||||
|
<div className="small text-muted mb-1">Сервер 1 ({server1.dns || server1.ip}):</div>
|
||||||
|
<div><strong>Имя:</strong> <code>{previewName1}</code></div>
|
||||||
|
<div><strong>Local IP:</strong> <code>{previewLocalIp || '—'}</code></div>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<div className="small text-muted mb-1">Сервер 2 ({server2.dns || server2.ip}):</div>
|
||||||
|
<div><strong>Имя:</strong> <code>{previewName2}</code></div>
|
||||||
|
<div><strong>Local IP:</strong> <code>{previewRemoteIp || '—'}</code></div>
|
||||||
|
</div>
|
||||||
|
{previewLocalIp && previewRemoteIp && (
|
||||||
|
<div className="col-12 mt-2">
|
||||||
|
<div className="badge bg-success-lt text-success">
|
||||||
|
{previewLocalIp} ↔ {previewRemoteIp}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
</FormModal>
|
||||||
|
|
||||||
{/* Pool Modal */}
|
{/* Pool Modal */}
|
||||||
<FormModal
|
<FormModal
|
||||||
show={poolModalOpen}
|
show={poolModalOpen}
|
||||||
|
|||||||
Reference in New Issue
Block a user