feat(OspfToolsPage): add cost optimization feature for OSPF interfaces with user notifications
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m3s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m3s
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { IconDatabase, IconRefresh, IconRoute2 } from '@tabler/icons-react';
|
import { IconDatabase, IconRefresh, IconRoute2, IconWand } from '@tabler/icons-react';
|
||||||
import PageHeader from './components/PageHeader.jsx';
|
import PageHeader from './components/PageHeader.jsx';
|
||||||
import api from './lib/api.js';
|
import api from './lib/api.js';
|
||||||
import { useNotify } from './components/NotifyProvider.jsx';
|
import { useNotify } from './components/NotifyProvider.jsx';
|
||||||
@@ -235,10 +235,12 @@ export default function OspfToolsPage() {
|
|||||||
const response = await api.get('/route-optimizer', { timeout: 30000 });
|
const response = await api.get('/route-optimizer', { timeout: 30000 });
|
||||||
const probabilities = buildOptimizerProbabilityMap(response?.data, servers);
|
const probabilities = buildOptimizerProbabilityMap(response?.data, servers);
|
||||||
setOptimizerProbabilities(probabilities);
|
setOptimizerProbabilities(probabilities);
|
||||||
|
return probabilities;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load route optimizer hints:', error);
|
console.error('Failed to load route optimizer hints:', error);
|
||||||
setOptimizerProbabilities({});
|
setOptimizerProbabilities({});
|
||||||
if (!silent) notify.warning('Не удалось загрузить рекомендации из оптимизатора маршрутов');
|
if (!silent) notify.warning('Не удалось загрузить рекомендации из оптимизатора маршрутов');
|
||||||
|
return {};
|
||||||
} finally {
|
} finally {
|
||||||
setOptimizerLoading(false);
|
setOptimizerLoading(false);
|
||||||
}
|
}
|
||||||
@@ -282,6 +284,66 @@ export default function OspfToolsPage() {
|
|||||||
setDragging(null);
|
setDragging(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const optimizeCosts = async () => {
|
||||||
|
let hints = optimalHints;
|
||||||
|
if (!hints || Object.keys(hints).length === 0) {
|
||||||
|
const probabilities = await loadOptimizerHints({ silent: true });
|
||||||
|
const refreshedHints = {};
|
||||||
|
groupedByRouter.forEach((routerGroup) => {
|
||||||
|
(routerGroup.areas || []).forEach((areaGroup) => {
|
||||||
|
const ranked = [...(areaGroup.items || [])]
|
||||||
|
.map((item) => {
|
||||||
|
const ifaceKey = `${routerGroup.routerKey}::${String(item.interfaceName || '').toUpperCase()}`;
|
||||||
|
const probabilityOptimal = Number((probabilities || {})[ifaceKey] || 0);
|
||||||
|
return { item, probabilityOptimal };
|
||||||
|
})
|
||||||
|
.sort((a, b) =>
|
||||||
|
Number(b.probabilityOptimal || 0) - Number(a.probabilityOptimal || 0) ||
|
||||||
|
String(a.item.interfaceName || '').localeCompare(String(b.item.interfaceName || ''))
|
||||||
|
);
|
||||||
|
ranked.forEach(({ item, probabilityOptimal }, idx) => {
|
||||||
|
const hintKey = `${routerGroup.routerKey}::${areaGroup.area}::${String(item.interfaceName || '').toUpperCase()}`;
|
||||||
|
refreshedHints[hintKey] = {
|
||||||
|
optimalCost: (idx + 1) * OSPF_COST_STEP,
|
||||||
|
probabilityOptimal,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
hints = refreshedHints;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hints || Object.keys(hints).length === 0) {
|
||||||
|
notify.warning('Нет рекомендаций оптимизатора для расчета cost');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let changedCount = 0;
|
||||||
|
let hintedCount = 0;
|
||||||
|
setTemplates((prev) =>
|
||||||
|
prev.map((item) => {
|
||||||
|
const hintKey = `${item.routerKey}::${item.area}::${String(item.interfaceName || '').toUpperCase()}`;
|
||||||
|
const hint = hints[hintKey];
|
||||||
|
if (!hint) return item;
|
||||||
|
hintedCount += 1;
|
||||||
|
const nextCost = Number(hint.optimalCost);
|
||||||
|
if (!Number.isFinite(nextCost) || nextCost < 0 || Number(item.cost) === nextCost) return item;
|
||||||
|
changedCount += 1;
|
||||||
|
return { ...item, cost: nextCost };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hintedCount === 0) {
|
||||||
|
notify.warning('Для интерфейсов на странице нет рекомендаций оптимизатора');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (changedCount === 0) {
|
||||||
|
notify.success('Текущие cost уже соответствуют оптимальным рекомендациям');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notify.success(`Оптимизация применена: обновлено cost у ${changedCount} интерфейсов`);
|
||||||
|
};
|
||||||
|
|
||||||
const saveOspf = async () => {
|
const saveOspf = async () => {
|
||||||
if (groupedByRouter.length === 0) {
|
if (groupedByRouter.length === 0) {
|
||||||
notify.warning('Нет OSPF данных для сохранения');
|
notify.warning('Нет OSPF данных для сохранения');
|
||||||
@@ -333,6 +395,16 @@ export default function OspfToolsPage() {
|
|||||||
<IconRefresh size={16} className={`me-1 ${(loading || optimizerLoading) ? 'spin' : ''}`} />
|
<IconRefresh size={16} className={`me-1 ${(loading || optimizerLoading) ? 'spin' : ''}`} />
|
||||||
Загрузить из MikroTik
|
Загрузить из MikroTik
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-primary"
|
||||||
|
onClick={optimizeCosts}
|
||||||
|
disabled={loading || optimizerLoading || groupedByRouter.length === 0}
|
||||||
|
title="Проставить интерфейсам потенциально оптимальный cost"
|
||||||
|
>
|
||||||
|
<IconWand size={16} className="me-1" />
|
||||||
|
Оптимизировать
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
|
|||||||
Reference in New Issue
Block a user