feat(mikrotik-config): add OSPF area comparison and enhance template application logic
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:
@@ -1489,6 +1489,12 @@ function sameOspfInterface(a, b) {
|
|||||||
return String(a || '').trim().toUpperCase() === String(b || '').trim().toUpperCase();
|
return String(a || '').trim().toUpperCase() === String(b || '').trim().toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sameOspfArea(a, b) {
|
||||||
|
const left = String(a || '').trim().toUpperCase() || 'BACKBONE';
|
||||||
|
const right = String(b || '').trim().toUpperCase() || 'BACKBONE';
|
||||||
|
return left === right;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/mikrotik/ospf-interface-templates?serverId=
|
* GET /api/mikrotik/ospf-interface-templates?serverId=
|
||||||
* Возвращает текущие OSPF interface-template с HOME роутеров.
|
* Возвращает текущие OSPF interface-template с HOME роутеров.
|
||||||
@@ -1567,7 +1573,7 @@ async function getOspfInterfaceTemplates(req, res) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/mikrotik/ospf-interface-templates/apply
|
* POST /api/mikrotik/ospf-interface-templates/apply
|
||||||
* Body: { servers: [{ serverId, templates: [{ interfaceName, cost }] }] }
|
* Body: { servers: [{ serverId, templates: [{ rosId?, interfaceName, area?, cost }] }] }
|
||||||
*/
|
*/
|
||||||
async function applyOspfInterfaceTemplates(req, res) {
|
async function applyOspfInterfaceTemplates(req, res) {
|
||||||
try {
|
try {
|
||||||
@@ -1611,17 +1617,27 @@ async function applyOspfInterfaceTemplates(req, res) {
|
|||||||
let invalid = 0;
|
let invalid = 0;
|
||||||
|
|
||||||
for (const desired of desiredTemplates) {
|
for (const desired of desiredTemplates) {
|
||||||
|
const desiredRosId = String(desired?.rosId || '').trim();
|
||||||
const interfaceName = String(desired?.interfaceName || '').trim();
|
const interfaceName = String(desired?.interfaceName || '').trim();
|
||||||
|
const desiredArea = String(desired?.area || '').trim();
|
||||||
const desiredCost = parseOspfCost(desired?.cost);
|
const desiredCost = parseOspfCost(desired?.cost);
|
||||||
|
|
||||||
if (!interfaceName || desiredCost == null) {
|
if ((!interfaceName && !desiredRosId) || desiredCost == null) {
|
||||||
invalid += 1;
|
invalid += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = existing.find((item) =>
|
let match = null;
|
||||||
sameOspfInterface(normalizeOspfInterfaceName(item), interfaceName)
|
if (desiredRosId) {
|
||||||
);
|
match = existing.find((item) => String(item?.['.id'] || '') === desiredRosId);
|
||||||
|
}
|
||||||
|
if (!match && interfaceName) {
|
||||||
|
match = existing.find((item) => {
|
||||||
|
if (!sameOspfInterface(normalizeOspfInterfaceName(item), interfaceName)) return false;
|
||||||
|
if (!desiredArea) return true;
|
||||||
|
return sameOspfArea(item?.area, desiredArea);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!match || !match['.id']) {
|
if (!match || !match['.id']) {
|
||||||
missing += 1;
|
missing += 1;
|
||||||
|
|||||||
@@ -127,160 +127,45 @@ const GATEWAY_TEMPLATES = [
|
|||||||
|
|
||||||
const OSPF_COST_STEP = 10;
|
const OSPF_COST_STEP = 10;
|
||||||
|
|
||||||
function getServerRefMap(servers = []) {
|
function buildOspfTemplatesFromRouterResults(routerResults = [], servers = []) {
|
||||||
const byRef = new Map();
|
const serverById = new Map((servers || []).map((s) => [String(s.id || s.ip || s.dns || ''), s]));
|
||||||
(servers || []).forEach((server) => {
|
const byComposite = new Map();
|
||||||
if (!server || typeof server !== 'object') return;
|
|
||||||
if (server.id) byRef.set(String(server.id), server);
|
|
||||||
if (server.ip) byRef.set(String(server.ip), server);
|
|
||||||
if (server.dns) byRef.set(String(server.dns), server);
|
|
||||||
});
|
|
||||||
return byRef;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHomeOspfCandidates(tunnelInterfaces = [], servers = []) {
|
(routerResults || []).forEach((result) => {
|
||||||
const serverByRef = getServerRefMap(servers);
|
const serverId = String(result?.serverId || '').trim();
|
||||||
const candidatesByServer = {};
|
if (!serverId) return;
|
||||||
|
const server = serverById.get(serverId) || {
|
||||||
|
id: serverId,
|
||||||
|
ip: String(result?.serverLabel || serverId),
|
||||||
|
dns: String(result?.serverLabel || ''),
|
||||||
|
};
|
||||||
|
const identity = getOspfRouterIdentity(server);
|
||||||
|
const templates = Array.isArray(result?.templates) ? result.templates : [];
|
||||||
|
|
||||||
(tunnelInterfaces || []).forEach((iface) => {
|
templates.forEach((tpl) => {
|
||||||
if (String(iface?.type || '').toUpperCase() !== 'GRE') return;
|
const interfaceName = String(tpl?.interfaceName || '').trim();
|
||||||
const tunnelId = String(iface?.id || '');
|
const area = String(tpl?.area || '').trim() || 'backbone';
|
||||||
|
const numericCost = Number(tpl?.cost);
|
||||||
const addCandidate = (serverRef, side) => {
|
const cost = Number.isFinite(numericCost) && numericCost >= 0 ? numericCost : OSPF_COST_STEP;
|
||||||
const server = serverByRef.get(String(serverRef || ''));
|
|
||||||
if (!server || String(server.type || '').toLowerCase() !== 'home') return;
|
|
||||||
|
|
||||||
const serverId = String(server.id || serverRef || '');
|
|
||||||
if (!serverId) return;
|
|
||||||
|
|
||||||
const interfaceName = String(
|
|
||||||
side === 'left' ? (iface?.name || '') : (iface?.name2 || iface?.name || '')
|
|
||||||
).trim();
|
|
||||||
if (!interfaceName) return;
|
if (!interfaceName) return;
|
||||||
|
|
||||||
const uniqueKey = `${tunnelId}:${side}:${interfaceName}`;
|
const compositeKey = `${identity.key}::${area.toLowerCase()}::${interfaceName.toUpperCase()}`;
|
||||||
if (!candidatesByServer[serverId]) candidatesByServer[serverId] = [];
|
if (byComposite.has(compositeKey)) return;
|
||||||
candidatesByServer[serverId].push({
|
|
||||||
key: uniqueKey,
|
byComposite.set(compositeKey, {
|
||||||
|
key: compositeKey,
|
||||||
|
rosId: tpl?.id || null,
|
||||||
serverId,
|
serverId,
|
||||||
tunnelInterfaceId: tunnelId,
|
routerKey: identity.key,
|
||||||
|
routerLabel: identity.label,
|
||||||
interfaceName,
|
interfaceName,
|
||||||
});
|
area,
|
||||||
};
|
cost,
|
||||||
|
|
||||||
addCandidate(iface?.serverId, 'left');
|
|
||||||
addCandidate(iface?.serverId2, 'right');
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.keys(candidatesByServer).forEach((serverId) => {
|
|
||||||
const unique = new Map();
|
|
||||||
candidatesByServer[serverId].forEach((candidate) => {
|
|
||||||
if (!unique.has(candidate.key)) unique.set(candidate.key, candidate);
|
|
||||||
});
|
|
||||||
candidatesByServer[serverId] = Array.from(unique.values());
|
|
||||||
});
|
|
||||||
|
|
||||||
return candidatesByServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
function areOspfTemplateListsEqual(prevList = [], nextList = []) {
|
|
||||||
if (prevList.length !== nextList.length) return false;
|
|
||||||
for (let i = 0; i < prevList.length; i += 1) {
|
|
||||||
const prev = prevList[i];
|
|
||||||
const next = nextList[i];
|
|
||||||
if (
|
|
||||||
String(prev?.id || '') !== String(next?.id || '') ||
|
|
||||||
String(prev?.key || '') !== String(next?.key || '') ||
|
|
||||||
String(prev?.serverId || '') !== String(next?.serverId || '') ||
|
|
||||||
String(prev?.tunnelInterfaceId || '') !== String(next?.tunnelInterfaceId || '') ||
|
|
||||||
String(prev?.interfaceName || '') !== String(next?.interfaceName || '') ||
|
|
||||||
Number(prev?.cost || 0) !== Number(next?.cost || 0)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeOspfTemplates(rawTemplates = [], candidatesByServer = {}) {
|
|
||||||
const normalized = [];
|
|
||||||
|
|
||||||
Object.entries(candidatesByServer).forEach(([serverId, candidates]) => {
|
|
||||||
const byKey = new Map(candidates.map((item) => [String(item.key), item]));
|
|
||||||
const existing = (rawTemplates || [])
|
|
||||||
.filter((item) => String(item?.serverId || '') === String(serverId) && byKey.has(String(item?.key || '')))
|
|
||||||
.sort((a, b) => Number(b?.cost || 0) - Number(a?.cost || 0));
|
|
||||||
|
|
||||||
const usedKeys = new Set();
|
|
||||||
const ordered = [];
|
|
||||||
|
|
||||||
existing.forEach((item) => {
|
|
||||||
const key = String(item?.key || '');
|
|
||||||
if (usedKeys.has(key)) return;
|
|
||||||
usedKeys.add(key);
|
|
||||||
ordered.push({
|
|
||||||
...item,
|
|
||||||
...byKey.get(key),
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
candidates.forEach((candidate) => {
|
|
||||||
if (usedKeys.has(candidate.key)) return;
|
|
||||||
usedKeys.add(candidate.key);
|
|
||||||
ordered.push({
|
|
||||||
id: `ospf-${serverId}-${candidate.key}`,
|
|
||||||
key: candidate.key,
|
|
||||||
serverId: candidate.serverId,
|
|
||||||
tunnelInterfaceId: candidate.tunnelInterfaceId,
|
|
||||||
interfaceName: candidate.interfaceName,
|
|
||||||
cost: null,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
return Array.from(byComposite.values());
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
function moveInArray(arr, fromIndex, toIndex) {
|
||||||
@@ -665,23 +550,6 @@ function NetworkConfigManager() {
|
|||||||
return result;
|
return result;
|
||||||
}, [config.ipPools, serverFilter, searchTerm, servers]);
|
}, [config.ipPools, serverFilter, searchTerm, servers]);
|
||||||
|
|
||||||
const homeOspfCandidatesByServer = useMemo(
|
|
||||||
() => getHomeOspfCandidates(config.tunnelInterfaces || [], servers),
|
|
||||||
[config.tunnelInterfaces, servers]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setConfig((prev) => {
|
|
||||||
const prevTemplates = Array.isArray(prev?.ospfInterfaceTemplates) ? prev.ospfInterfaceTemplates : [];
|
|
||||||
const nextTemplates = normalizeOspfTemplates(prevTemplates, homeOspfCandidatesByServer);
|
|
||||||
if (areOspfTemplateListsEqual(prevTemplates, nextTemplates)) return prev;
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
ospfInterfaceTemplates: nextTemplates,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, [homeOspfCandidatesByServer]);
|
|
||||||
|
|
||||||
const ospfTemplatesByRouter = useMemo(() => {
|
const ospfTemplatesByRouter = useMemo(() => {
|
||||||
const grouped = {};
|
const grouped = {};
|
||||||
(config.ospfInterfaceTemplates || []).forEach((item) => {
|
(config.ospfInterfaceTemplates || []).forEach((item) => {
|
||||||
@@ -703,11 +571,24 @@ function NetworkConfigManager() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const groups = Object.values(grouped).map((group) => {
|
const groups = Object.values(grouped).map((group) => {
|
||||||
const sorted = [...group.items].sort((a, b) => Number(b?.cost || 0) - Number(a?.cost || 0));
|
const areasMap = {};
|
||||||
|
group.items.forEach((item) => {
|
||||||
|
const area = String(item?.area || '').trim() || 'backbone';
|
||||||
|
if (!areasMap[area]) areasMap[area] = [];
|
||||||
|
areasMap[area].push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
const areas = Object.entries(areasMap)
|
||||||
|
.map(([area, items]) => ({
|
||||||
|
area,
|
||||||
|
items: [...items].sort((a, b) => Number(b?.cost || 0) - Number(a?.cost || 0)),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.area.localeCompare(b.area));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...group,
|
...group,
|
||||||
serverIds: Array.from(group.serverIds),
|
serverIds: Array.from(group.serverIds),
|
||||||
items: sorted,
|
areas,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -719,8 +600,9 @@ function NetworkConfigManager() {
|
|||||||
return groups;
|
return groups;
|
||||||
}, [config.ospfInterfaceTemplates, serverFilter, servers]);
|
}, [config.ospfInterfaceTemplates, serverFilter, servers]);
|
||||||
|
|
||||||
const handleDropOspfTemplate = (routerKey, targetKey) => {
|
const handleDropOspfTemplate = (routerKey, area, targetKey) => {
|
||||||
if (!ospfDraggingItem || String(ospfDraggingItem.routerKey) !== String(routerKey)) return;
|
if (!ospfDraggingItem || String(ospfDraggingItem.routerKey) !== String(routerKey)) return;
|
||||||
|
if (String(ospfDraggingItem.area || '') !== String(area || '')) return;
|
||||||
const draggedKey = String(ospfDraggingItem.key || '');
|
const draggedKey = String(ospfDraggingItem.key || '');
|
||||||
const destinationKey = String(targetKey || '');
|
const destinationKey = String(targetKey || '');
|
||||||
if (!draggedKey || !destinationKey || draggedKey === destinationKey) {
|
if (!draggedKey || !destinationKey || draggedKey === destinationKey) {
|
||||||
@@ -734,7 +616,8 @@ function NetworkConfigManager() {
|
|||||||
const sid = String(item.serverId || '');
|
const sid = String(item.serverId || '');
|
||||||
const server = getServerInfo(sid);
|
const server = getServerInfo(sid);
|
||||||
const identity = getOspfRouterIdentity(server || { id: sid, ip: sid });
|
const identity = getOspfRouterIdentity(server || { id: sid, ip: sid });
|
||||||
return identity.key === String(routerKey);
|
const itemArea = String(item?.area || '').trim() || 'backbone';
|
||||||
|
return identity.key === String(routerKey) && itemArea === String(area || 'backbone');
|
||||||
})
|
})
|
||||||
.sort((a, b) => Number(b?.cost || 0) - Number(a?.cost || 0));
|
.sort((a, b) => Number(b?.cost || 0) - Number(a?.cost || 0));
|
||||||
const fromIndex = current.findIndex((item) => String(item.key || '') === draggedKey);
|
const fromIndex = current.findIndex((item) => String(item.key || '') === draggedKey);
|
||||||
@@ -750,7 +633,8 @@ function NetworkConfigManager() {
|
|||||||
const sid = String(item.serverId || '');
|
const sid = String(item.serverId || '');
|
||||||
const server = getServerInfo(sid);
|
const server = getServerInfo(sid);
|
||||||
const identity = getOspfRouterIdentity(server || { id: sid, ip: sid });
|
const identity = getOspfRouterIdentity(server || { id: sid, ip: sid });
|
||||||
return identity.key !== String(routerKey);
|
const itemArea = String(item?.area || '').trim() || 'backbone';
|
||||||
|
return identity.key !== String(routerKey) || itemArea !== String(area || 'backbone');
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -768,10 +652,11 @@ function NetworkConfigManager() {
|
|||||||
const response = await api.get('/mikrotik/ospf-interface-templates');
|
const response = await api.get('/mikrotik/ospf-interface-templates');
|
||||||
const results = Array.isArray(response.data?.results) ? response.data.results : [];
|
const results = Array.isArray(response.data?.results) ? response.data.results : [];
|
||||||
const errors = Array.isArray(response.data?.errors) ? response.data.errors : [];
|
const errors = Array.isArray(response.data?.errors) ? response.data.errors : [];
|
||||||
|
const loadedTemplates = buildOspfTemplatesFromRouterResults(results, servers);
|
||||||
|
|
||||||
setConfig((prev) => ({
|
setConfig((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
ospfInterfaceTemplates: mergeOspfCostsFromRouter(prev.ospfInterfaceTemplates || [], results),
|
ospfInterfaceTemplates: loadedTemplates,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (!silent && results.length > 0) {
|
if (!silent && results.length > 0) {
|
||||||
@@ -803,15 +688,21 @@ function NetworkConfigManager() {
|
|||||||
const payload = {
|
const payload = {
|
||||||
servers: ospfTemplatesByRouter.map((group) => {
|
servers: ospfTemplatesByRouter.map((group) => {
|
||||||
const dedup = new Map();
|
const dedup = new Map();
|
||||||
group.items.forEach((item) => {
|
(group.areas || []).forEach((areaGroup) => {
|
||||||
const key = String(item.interfaceName || '').trim().toUpperCase();
|
(areaGroup.items || []).forEach((item) => {
|
||||||
if (!key) return;
|
const interfaceName = String(item.interfaceName || '').trim();
|
||||||
if (!dedup.has(key)) {
|
const area = String(item.area || '').trim() || 'backbone';
|
||||||
dedup.set(key, {
|
if (!interfaceName) return;
|
||||||
interfaceName: item.interfaceName,
|
const key = `${area.toUpperCase()}::${interfaceName.toUpperCase()}`;
|
||||||
cost: item.cost,
|
if (!dedup.has(key)) {
|
||||||
});
|
dedup.set(key, {
|
||||||
}
|
rosId: item.rosId || null,
|
||||||
|
interfaceName,
|
||||||
|
area,
|
||||||
|
cost: item.cost,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
serverId: group.representativeServerId,
|
serverId: group.representativeServerId,
|
||||||
@@ -842,10 +733,11 @@ function NetworkConfigManager() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeTab !== 'interfaces') return;
|
if (activeTab !== 'interfaces') return;
|
||||||
|
if (!Array.isArray(servers) || servers.length === 0) return;
|
||||||
if (ospfAutoLoadedRef.current) return;
|
if (ospfAutoLoadedRef.current) return;
|
||||||
ospfAutoLoadedRef.current = true;
|
ospfAutoLoadedRef.current = true;
|
||||||
handleLoadOspfFromMikrotik({ silent: true });
|
handleLoadOspfFromMikrotik({ silent: true });
|
||||||
}, [activeTab]);
|
}, [activeTab, servers]);
|
||||||
|
|
||||||
// === Группировка по серверам ===
|
// === Группировка по серверам ===
|
||||||
const gatewaysByServer = useMemo(() => {
|
const gatewaysByServer = useMemo(() => {
|
||||||
@@ -4036,7 +3928,7 @@ function NetworkConfigManager() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="card-title mb-0">OSPF Interface Template (HOME)</h3>
|
<h3 className="card-title mb-0">OSPF Interface Template (HOME)</h3>
|
||||||
<div className="text-muted small">
|
<div className="text-muted small">
|
||||||
Только для HOME роутеров. Перетаскивание меняет приоритет: чем выше cost, тем выше приоритет.
|
Только для HOME роутеров. Источник данных — MikroTik, группировка по Area.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="d-flex align-items-center gap-2">
|
<div className="d-flex align-items-center gap-2">
|
||||||
@@ -4068,44 +3960,59 @@ function NetworkConfigManager() {
|
|||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
{ospfTemplatesByRouter.length === 0 ? (
|
{ospfTemplatesByRouter.length === 0 ? (
|
||||||
<div className="text-muted">
|
<div className="text-muted">
|
||||||
Для HOME роутеров пока нет GRE-интерфейсов. Создайте GRE-туннели, и шаблоны появятся автоматически.
|
OSPF Interface Template не найдены на HOME роутерах.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="row g-3">
|
<div className="row g-3">
|
||||||
{ospfTemplatesByRouter.map((group) => {
|
{ospfTemplatesByRouter.map((group) => {
|
||||||
const templates = group.items || [];
|
const areas = group.areas || [];
|
||||||
return (
|
return (
|
||||||
<div key={`ospf-router-${group.routerKey}`} className="col-12 col-xl-6">
|
<div key={`ospf-router-${group.routerKey}`} className="col-12 col-xl-6">
|
||||||
<div className="card card-sm border">
|
<div className="card card-sm border">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
<div className="fw-semibold">{group.routerLabel}</div>
|
<div className="fw-semibold">{group.routerLabel}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="list-group list-group-flush">
|
<div className="card-body p-0">
|
||||||
{templates.length === 0 ? (
|
{areas.length === 0 ? (
|
||||||
<div className="list-group-item text-muted small">
|
<div className="list-group-item text-muted small">
|
||||||
Нет доступных интерфейсов для OSPF шаблонов.
|
Нет доступных интерфейсов для OSPF шаблонов.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
templates.map((template) => (
|
areas.map((areaGroup) => (
|
||||||
<div
|
<div key={`${group.routerKey}-${areaGroup.area}`} className="border-top">
|
||||||
key={template.key}
|
<div className="px-3 py-2 bg-light text-muted small fw-semibold">
|
||||||
className="list-group-item d-flex align-items-center justify-content-between"
|
Area: {areaGroup.area}
|
||||||
draggable
|
</div>
|
||||||
onDragStart={() =>
|
<div className="list-group list-group-flush">
|
||||||
setOspfDraggingItem({ routerKey: group.routerKey, key: template.key })
|
{areaGroup.items.map((template) => (
|
||||||
}
|
<div
|
||||||
onDragOver={(e) => e.preventDefault()}
|
key={template.key}
|
||||||
onDrop={() => handleDropOspfTemplate(group.routerKey, template.key)}
|
className="list-group-item d-flex align-items-center justify-content-between"
|
||||||
onDragEnd={() => setOspfDraggingItem(null)}
|
draggable
|
||||||
style={{ cursor: 'grab' }}
|
onDragStart={() =>
|
||||||
>
|
setOspfDraggingItem({
|
||||||
<div className="d-flex align-items-center gap-2">
|
routerKey: group.routerKey,
|
||||||
<span className="text-muted small">::</span>
|
area: areaGroup.area,
|
||||||
<code>{template.interfaceName}</code>
|
key: template.key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onDragOver={(e) => e.preventDefault()}
|
||||||
|
onDrop={() =>
|
||||||
|
handleDropOspfTemplate(group.routerKey, areaGroup.area, template.key)
|
||||||
|
}
|
||||||
|
onDragEnd={() => setOspfDraggingItem(null)}
|
||||||
|
style={{ cursor: 'grab' }}
|
||||||
|
>
|
||||||
|
<div className="d-flex align-items-center gap-2">
|
||||||
|
<span className="text-muted small">::</span>
|
||||||
|
<code>{template.interfaceName}</code>
|
||||||
|
</div>
|
||||||
|
<span className="badge bg-primary-lt text-primary">
|
||||||
|
cost {template.cost}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<span className="badge bg-primary-lt text-primary">
|
|
||||||
cost {template.cost}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user