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

This commit is contained in:
2026-03-05 15:43:02 +07:00
parent 113fa59a63
commit e631ffab51
3 changed files with 338 additions and 9 deletions
+136 -8
View File
@@ -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 ? (