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

This commit is contained in:
2026-01-22 18:43:59 +07:00
parent 566dd1d1bb
commit 011740df1f
+218 -4
View File
@@ -20,6 +20,7 @@ import {
IconSearch,
IconRefresh,
IconWand,
IconSparkles,
IconFilter,
IconX,
IconCircleFilled,
@@ -131,6 +132,13 @@ function NetworkConfigManager() {
const [itemToDelete, setItemToDelete] = useState(null);
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 ===
const [mikrotikCodeModalOpen, setMikrotikCodeModalOpen] = useState(false);
const [generatedMikrotikCode, setGeneratedMikrotikCode] = useState([]);
@@ -432,6 +440,86 @@ function NetworkConfigManager() {
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 ===
const handleAddInterface = () => {
setEditingInterface(getEmptyInterface());
@@ -1771,10 +1859,20 @@ function NetworkConfigManager() {
</button>
)}
{activeTab === 'interfaces' && (
<button className="btn btn-primary btn-sm" onClick={handleAddInterface}>
<IconPlus size={16} className="me-1" />
Интерфейс
</button>
<>
<button className="btn btn-primary btn-sm" onClick={handleAddInterface}>
<IconPlus size={16} className="me-1" />
Интерфейс
</button>
<button
className="btn btn-outline-primary btn-sm"
onClick={() => setInterfaceTemplateModalOpen(true)}
title="Создать интерфейс из шаблона"
>
<IconSparkles size={16} className="me-1" />
Шаблон
</button>
</>
)}
{activeTab === 'pools' && (
<button className="btn btn-primary btn-sm" onClick={handleAddPool}>
@@ -2642,6 +2740,122 @@ function NetworkConfigManager() {
)}
</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 */}
<FormModal
show={poolModalOpen}