feat(mikrotik-config): add OSPF interface templates management with loading and applying functionality
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m35s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m35s
This commit is contained in:
@@ -233,19 +233,56 @@ function normalizeOspfTemplates(rawTemplates = [], candidatesByServer = {}) {
|
||||
serverId: candidate.serverId,
|
||||
tunnelInterfaceId: candidate.tunnelInterfaceId,
|
||||
interfaceName: candidate.interfaceName,
|
||||
cost: null,
|
||||
});
|
||||
});
|
||||
|
||||
const withCosts = ordered.map((item, idx) => ({
|
||||
...item,
|
||||
cost: (ordered.length - idx) * OSPF_COST_STEP,
|
||||
}));
|
||||
normalized.push(...withCosts);
|
||||
const existingCosts = ordered
|
||||
.map((item) => Number(item?.cost))
|
||||
.filter((cost) => Number.isFinite(cost) && cost >= 0);
|
||||
let nextAutoCost = existingCosts.length > 0
|
||||
? Math.max(OSPF_COST_STEP, Math.min(...existingCosts) - OSPF_COST_STEP)
|
||||
: Math.max(OSPF_COST_STEP, ordered.length * OSPF_COST_STEP);
|
||||
|
||||
ordered.forEach((item) => {
|
||||
const numericCost = Number(item?.cost);
|
||||
if (Number.isFinite(numericCost) && numericCost >= 0) {
|
||||
normalized.push({ ...item, cost: numericCost });
|
||||
return;
|
||||
}
|
||||
normalized.push({ ...item, cost: nextAutoCost });
|
||||
nextAutoCost = Math.max(OSPF_COST_STEP, nextAutoCost - OSPF_COST_STEP);
|
||||
});
|
||||
});
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function mergeOspfCostsFromRouter(currentTemplates = [], routerResults = []) {
|
||||
const routerCostMap = new Map();
|
||||
|
||||
(routerResults || []).forEach((serverResult) => {
|
||||
const serverId = String(serverResult?.serverId || '').trim();
|
||||
if (!serverId) return;
|
||||
const templates = Array.isArray(serverResult?.templates) ? serverResult.templates : [];
|
||||
templates.forEach((item) => {
|
||||
const interfaceName = String(item?.interfaceName || '').trim();
|
||||
const cost = Number(item?.cost);
|
||||
if (!interfaceName || !Number.isFinite(cost) || cost < 0) return;
|
||||
const key = `${serverId}::${interfaceName.toUpperCase()}`;
|
||||
routerCostMap.set(key, cost);
|
||||
});
|
||||
});
|
||||
|
||||
return (currentTemplates || []).map((item) => {
|
||||
const serverId = String(item?.serverId || '').trim();
|
||||
const interfaceName = String(item?.interfaceName || '').trim();
|
||||
const lookup = `${serverId}::${interfaceName.toUpperCase()}`;
|
||||
if (!routerCostMap.has(lookup)) return item;
|
||||
return { ...item, cost: routerCostMap.get(lookup) };
|
||||
});
|
||||
}
|
||||
|
||||
function moveInArray(arr, fromIndex, toIndex) {
|
||||
if (fromIndex < 0 || toIndex < 0 || fromIndex >= arr.length || toIndex >= arr.length) return arr;
|
||||
const copy = [...arr];
|
||||
@@ -353,6 +390,8 @@ function NetworkConfigManager() {
|
||||
const [templateTunnelCount, setTemplateTunnelCount] = useState(1); // Количество туннелей для создания
|
||||
const [templateServerPairs, setTemplateServerPairs] = useState([]); // Массив пар серверов для множественного создания
|
||||
const [ospfDraggingItem, setOspfDraggingItem] = useState(null);
|
||||
const [ospfLoading, setOspfLoading] = useState(false);
|
||||
const [ospfSaving, setOspfSaving] = useState(false);
|
||||
|
||||
// === IPSec Passwords Management ===
|
||||
const [ipsecPasswords, setIpsecPasswords] = useState([]);
|
||||
@@ -685,6 +724,73 @@ function NetworkConfigManager() {
|
||||
setOspfDraggingItem(null);
|
||||
};
|
||||
|
||||
const handleLoadOspfFromMikrotik = async () => {
|
||||
setOspfLoading(true);
|
||||
try {
|
||||
const response = await api.get('/mikrotik/ospf-interface-templates');
|
||||
const results = Array.isArray(response.data?.results) ? response.data.results : [];
|
||||
const errors = Array.isArray(response.data?.errors) ? response.data.errors : [];
|
||||
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
ospfInterfaceTemplates: mergeOspfCostsFromRouter(prev.ospfInterfaceTemplates || [], results),
|
||||
}));
|
||||
|
||||
if (results.length > 0) {
|
||||
notify.success(`OSPF загружен с HOME роутеров: ${results.length}`);
|
||||
} else {
|
||||
notify.warning('Не удалось получить OSPF шаблоны ни с одного HOME роутера');
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
notify.warning(`Ошибки чтения OSPF: ${errors.length} роутер(ов)`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading OSPF templates:', error);
|
||||
notify.error('Не удалось загрузить OSPF Interface Template с MikroTik');
|
||||
} finally {
|
||||
setOspfLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveOspfToMikrotik = async () => {
|
||||
const serverEntries = Object.entries(ospfTemplatesByHomeServer);
|
||||
if (serverEntries.length === 0) {
|
||||
notify.warning('Нет OSPF шаблонов для сохранения');
|
||||
return;
|
||||
}
|
||||
|
||||
setOspfSaving(true);
|
||||
try {
|
||||
const payload = {
|
||||
servers: serverEntries.map(([serverId, templates]) => ({
|
||||
serverId,
|
||||
templates: templates.map((item) => ({
|
||||
interfaceName: item.interfaceName,
|
||||
cost: item.cost,
|
||||
})),
|
||||
})),
|
||||
};
|
||||
|
||||
const response = await api.post('/mikrotik/ospf-interface-templates/apply', payload);
|
||||
const summary = response.data?.summary || {};
|
||||
const errors = Array.isArray(response.data?.errors) ? response.data.errors : [];
|
||||
notify.success(
|
||||
`OSPF сохранен: обновлено ${summary.updated || 0}, пропущено ${summary.skipped || 0}, не найдено ${summary.missing || 0}`
|
||||
);
|
||||
if (Number(summary.invalid || 0) > 0) {
|
||||
notify.warning(`Некорректных записей OSPF: ${summary.invalid}`);
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
notify.warning(`Ошибки применения OSPF: ${errors.length} роутер(ов)`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error applying OSPF templates:', error);
|
||||
notify.error('Не удалось сохранить OSPF Interface Template в MikroTik');
|
||||
} finally {
|
||||
setOspfSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// === Группировка по серверам ===
|
||||
const gatewaysByServer = useMemo(() => {
|
||||
const grouped = {};
|
||||
@@ -3877,9 +3983,31 @@ function NetworkConfigManager() {
|
||||
Только для HOME роутеров. Перетаскивание меняет приоритет: чем выше cost, тем выше приоритет.
|
||||
</div>
|
||||
</div>
|
||||
<span className="badge bg-blue-lt text-blue">
|
||||
{config.ospfInterfaceTemplates?.length || 0}
|
||||
</span>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<span className="badge bg-blue-lt text-blue">
|
||||
{config.ospfInterfaceTemplates?.length || 0}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-secondary btn-sm"
|
||||
onClick={handleLoadOspfFromMikrotik}
|
||||
disabled={ospfLoading}
|
||||
title="Загрузить актуальные OSPF Interface Template с HOME роутеров"
|
||||
>
|
||||
<IconRefresh size={14} className={`me-1 ${ospfLoading ? 'spin' : ''}`} />
|
||||
Загрузить из MikroTik
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={handleSaveOspfToMikrotik}
|
||||
disabled={ospfSaving || (config.ospfInterfaceTemplates?.length || 0) === 0}
|
||||
title="Применить cost из шаблона на HOME роутеры"
|
||||
>
|
||||
<IconDatabase size={14} className={`me-1 ${ospfSaving ? 'spin' : ''}`} />
|
||||
Сохранить OSPF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{ospfHomeServerIds.length === 0 ? (
|
||||
|
||||
Reference in New Issue
Block a user