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,
|
||||
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}
|
||||
|
||||
Reference in New Issue
Block a user