feat(OspfToolsPage): implement optimal cost hints for OSPF routes and enhance loading logic
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m2s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m2s
This commit is contained in:
@@ -76,13 +76,72 @@ function buildOspfTemplatesFromRouterResults(routerResults = [], servers = []) {
|
|||||||
return Array.from(byComposite.values());
|
return Array.from(byComposite.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildOptimalCostHints(routeOptimizerData, servers = []) {
|
||||||
|
const homes = Array.isArray(routeOptimizerData?.homes) ? routeOptimizerData.homes : [];
|
||||||
|
const byRef = new Map();
|
||||||
|
(servers || []).forEach((server) => {
|
||||||
|
[server?.id, server?.ip, server?.dns]
|
||||||
|
.filter(Boolean)
|
||||||
|
.forEach((ref) => byRef.set(String(ref), server));
|
||||||
|
});
|
||||||
|
|
||||||
|
const hints = {};
|
||||||
|
|
||||||
|
homes.forEach((entry) => {
|
||||||
|
const homeRefCandidates = [
|
||||||
|
entry?.home?.id,
|
||||||
|
entry?.home?.ip,
|
||||||
|
entry?.home?.dns,
|
||||||
|
].filter(Boolean).map(String);
|
||||||
|
const homeServer = homeRefCandidates.map((ref) => byRef.get(ref)).find(Boolean);
|
||||||
|
if (!homeServer) return;
|
||||||
|
|
||||||
|
const identity = getOspfRouterIdentity(homeServer);
|
||||||
|
const candidates = Array.isArray(entry?.homeToJumphostCandidates)
|
||||||
|
? [...entry.homeToJumphostCandidates]
|
||||||
|
: [];
|
||||||
|
candidates.sort((a, b) =>
|
||||||
|
Number(b?.probabilityOptimal || 0) - Number(a?.probabilityOptimal || 0) ||
|
||||||
|
Number(b?.score || 0) - Number(a?.score || 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
const uniqueIface = [];
|
||||||
|
const seen = new Set();
|
||||||
|
candidates.forEach((candidate) => {
|
||||||
|
const iface = String(candidate?.interfaceName || '').trim();
|
||||||
|
if (!iface) return;
|
||||||
|
const key = iface.toUpperCase();
|
||||||
|
if (seen.has(key)) return;
|
||||||
|
seen.add(key);
|
||||||
|
uniqueIface.push(candidate);
|
||||||
|
});
|
||||||
|
|
||||||
|
uniqueIface.forEach((candidate, idx) => {
|
||||||
|
const iface = String(candidate?.interfaceName || '').trim();
|
||||||
|
const hintKey = `${identity.key}::${iface.toUpperCase()}`;
|
||||||
|
const nextHint = {
|
||||||
|
optimalCost: (idx + 1) * OSPF_COST_STEP,
|
||||||
|
probabilityOptimal: Number(candidate?.probabilityOptimal || 0),
|
||||||
|
};
|
||||||
|
const prev = hints[hintKey];
|
||||||
|
if (!prev || nextHint.probabilityOptimal > prev.probabilityOptimal) {
|
||||||
|
hints[hintKey] = nextHint;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return hints;
|
||||||
|
}
|
||||||
|
|
||||||
export default function OspfToolsPage() {
|
export default function OspfToolsPage() {
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
const [servers, setServers] = useState([]);
|
const [servers, setServers] = useState([]);
|
||||||
const [templates, setTemplates] = useState([]);
|
const [templates, setTemplates] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [optimizerLoading, setOptimizerLoading] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [dragging, setDragging] = useState(null);
|
const [dragging, setDragging] = useState(null);
|
||||||
|
const [optimalHints, setOptimalHints] = useState({});
|
||||||
|
|
||||||
const groupedByRouter = useMemo(() => {
|
const groupedByRouter = useMemo(() => {
|
||||||
const grouped = {};
|
const grouped = {};
|
||||||
@@ -144,6 +203,21 @@ export default function OspfToolsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadOptimizerHints = async ({ silent = false } = {}) => {
|
||||||
|
setOptimizerLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await api.get('/route-optimizer', { timeout: 30000 });
|
||||||
|
const hints = buildOptimalCostHints(response?.data, servers);
|
||||||
|
setOptimalHints(hints);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load route optimizer hints:', error);
|
||||||
|
setOptimalHints({});
|
||||||
|
if (!silent) notify.warning('Не удалось загрузить рекомендации из оптимизатора маршрутов');
|
||||||
|
} finally {
|
||||||
|
setOptimizerLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadServers();
|
loadServers();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -151,6 +225,7 @@ export default function OspfToolsPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (servers.length === 0) return;
|
if (servers.length === 0) return;
|
||||||
loadOspf({ silent: true });
|
loadOspf({ silent: true });
|
||||||
|
loadOptimizerHints({ silent: true });
|
||||||
}, [servers.length]);
|
}, [servers.length]);
|
||||||
|
|
||||||
const onDrop = (routerKey, area, targetKey) => {
|
const onDrop = (routerKey, area, targetKey) => {
|
||||||
@@ -223,10 +298,13 @@ export default function OspfToolsPage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-outline-secondary"
|
className="btn btn-outline-secondary"
|
||||||
onClick={() => loadOspf()}
|
onClick={() => {
|
||||||
disabled={loading}
|
loadOspf();
|
||||||
|
loadOptimizerHints({ silent: true });
|
||||||
|
}}
|
||||||
|
disabled={loading || optimizerLoading}
|
||||||
>
|
>
|
||||||
<IconRefresh size={16} className={`me-1 ${loading ? 'spin' : ''}`} />
|
<IconRefresh size={16} className={`me-1 ${(loading || optimizerLoading) ? 'spin' : ''}`} />
|
||||||
Загрузить из MikroTik
|
Загрузить из MikroTik
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -246,7 +324,7 @@ export default function OspfToolsPage() {
|
|||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<div className="text-muted small">
|
<div className="text-muted small">
|
||||||
Данные OSPF берутся напрямую с MikroTik. Группировка: роутер → Area → интерфейсы.
|
Данные OSPF берутся напрямую с MikroTik. Группировка: роутер → Area → интерфейсы.
|
||||||
Чем меньше cost, тем выше приоритет.
|
Чем меньше cost, тем выше приоритет. Рядом отображается потенциально оптимальный cost из оптимизатора маршрутов.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
@@ -266,6 +344,10 @@ export default function OspfToolsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="list-group list-group-flush">
|
<div className="list-group list-group-flush">
|
||||||
{areaGroup.items.map((item) => (
|
{areaGroup.items.map((item) => (
|
||||||
|
(() => {
|
||||||
|
const hint = optimalHints[`${routerGroup.routerKey}::${String(item.interfaceName || '').toUpperCase()}`];
|
||||||
|
const hintMatches = hint && Number(hint.optimalCost) === Number(item.cost);
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.key}
|
key={item.key}
|
||||||
className="list-group-item d-flex align-items-center justify-content-between"
|
className="list-group-item d-flex align-items-center justify-content-between"
|
||||||
@@ -286,9 +368,19 @@ export default function OspfToolsPage() {
|
|||||||
<span className={`badge ${item.active ? 'bg-green-lt text-green' : 'bg-secondary-lt text-secondary'}`}>
|
<span className={`badge ${item.active ? 'bg-green-lt text-green' : 'bg-secondary-lt text-secondary'}`}>
|
||||||
{item.active ? 'Активен' : 'Неактивен'}
|
{item.active ? 'Активен' : 'Неактивен'}
|
||||||
</span>
|
</span>
|
||||||
|
{hint && (
|
||||||
|
<span
|
||||||
|
className={`badge ${hintMatches ? 'bg-green-lt text-green' : 'bg-yellow-lt text-yellow'}`}
|
||||||
|
title={`Вероятность оптимальности: ${hint.probabilityOptimal}%`}
|
||||||
|
>
|
||||||
|
opt cost {hint.optimalCost}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="badge bg-primary-lt text-primary">cost {item.cost}</span>
|
<span className="badge bg-primary-lt text-primary">cost {item.cost}</span>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
})()
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user