mirror of
https://github.com/hansjone/netx.git
synced 2026-10-12 10:30:49 +08:00
Ship gated eye polish, fabric levels, and collection UI refresh.
Topology MCP adds pull/compact/bundle/suggest-hubs with a no-template skill path; API gains fabric level and NE collection policy; web list pages get paging and denser collect/network workflows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b1aee86701
commit
b81e5869a6
91 changed files with 10395 additions and 1513 deletions
|
|
@ -291,6 +291,7 @@ export type TopologyReactFlowViewProps = {
|
|||
setCanvasZoom: (z: number) => void;
|
||||
scheduleFlatViewportRefresh: () => void;
|
||||
toggleFullscreen: () => void | Promise<void>;
|
||||
readOnly?: boolean;
|
||||
};
|
||||
|
||||
export default function TopologyReactFlowView(props: TopologyReactFlowViewProps) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type { NeNodeData } from "./TopologyReactFlowView";
|
|||
import type { ToolMode } from "./toolMode";
|
||||
|
||||
export type TopologyToolbarProps = {
|
||||
readOnly?: boolean;
|
||||
breadcrumbFolders: TopologyTreeFolderItem[];
|
||||
activeView: TopologyTreeViewItem | null | undefined;
|
||||
activeRegion: TopologyTreeFolderItem | null | undefined;
|
||||
|
|
@ -51,6 +52,7 @@ export type TopologyToolbarProps = {
|
|||
};
|
||||
|
||||
export function TopologyToolbar({
|
||||
readOnly = false,
|
||||
breadcrumbFolders,
|
||||
activeView,
|
||||
activeRegion,
|
||||
|
|
@ -186,8 +188,14 @@ export function TopologyToolbar({
|
|||
<button
|
||||
type="button"
|
||||
className="btn btn--sm"
|
||||
disabled={isWorldFlatCanvas}
|
||||
title={isWorldFlatCanvas ? t("topology.worldMapNoDirectNes") : undefined}
|
||||
disabled={readOnly || isWorldFlatCanvas}
|
||||
title={
|
||||
readOnly
|
||||
? t("topology.readOnlyHint")
|
||||
: isWorldFlatCanvas
|
||||
? t("topology.worldMapNoDirectNes")
|
||||
: undefined
|
||||
}
|
||||
onClick={onAddNe}
|
||||
>
|
||||
{t("topology.addNe")}
|
||||
|
|
@ -195,8 +203,14 @@ export function TopologyToolbar({
|
|||
<button
|
||||
type="button"
|
||||
className="btn btn--sm"
|
||||
disabled={isWorldFlatCanvas}
|
||||
title={isWorldFlatCanvas ? t("topology.worldMapNoDirectNes") : undefined}
|
||||
disabled={readOnly || isWorldFlatCanvas}
|
||||
title={
|
||||
readOnly
|
||||
? t("topology.readOnlyHint")
|
||||
: isWorldFlatCanvas
|
||||
? t("topology.worldMapNoDirectNes")
|
||||
: undefined
|
||||
}
|
||||
onClick={handleCreateNe}
|
||||
>
|
||||
{t("topology.createNe")}
|
||||
|
|
@ -207,7 +221,7 @@ export function TopologyToolbar({
|
|||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
disabled={!canUndo}
|
||||
disabled={readOnly || !canUndo}
|
||||
onClick={onUndo}
|
||||
title="Ctrl+Z"
|
||||
>
|
||||
|
|
@ -216,7 +230,7 @@ export function TopologyToolbar({
|
|||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
disabled={!canRedo}
|
||||
disabled={readOnly || !canRedo}
|
||||
onClick={onRedo}
|
||||
title="Ctrl+Y"
|
||||
>
|
||||
|
|
@ -225,9 +239,9 @@ export function TopologyToolbar({
|
|||
<button
|
||||
type="button"
|
||||
className={`btn btn--sm${dirty ? "" : " btn--ghost"}`}
|
||||
disabled={savePending || !dirty}
|
||||
disabled={readOnly || savePending || !dirty}
|
||||
onClick={onSave}
|
||||
title="Ctrl+S"
|
||||
title={readOnly ? t("topology.readOnlyHint") : "Ctrl+S"}
|
||||
>
|
||||
{savePending ? t("topology.saving") : dirty ? t("topology.saveDirty") : t("topology.save")}
|
||||
</button>
|
||||
|
|
@ -265,8 +279,8 @@ export function TopologyToolbar({
|
|||
<button
|
||||
type="button"
|
||||
className="btn btn--sm btn--ghost"
|
||||
disabled={!staleEdgeCount}
|
||||
title={t("topology.removeStaleHint")}
|
||||
disabled={readOnly || !staleEdgeCount}
|
||||
title={readOnly ? t("topology.readOnlyHint") : t("topology.removeStaleHint")}
|
||||
onClick={() => void onRemoveStale()}
|
||||
>
|
||||
{t("topology.removeStale").replace("{{count}}", String(staleEdgeCount))}
|
||||
|
|
@ -286,8 +300,9 @@ export function TopologyToolbar({
|
|||
key={mode}
|
||||
type="button"
|
||||
className={`topo-tools__btn${toolMode === mode ? " is-active" : ""}`}
|
||||
title={`${label} (${key})`}
|
||||
title={`${label} (${key})${readOnly && mode === "connect" ? ` — ${t("topology.readOnlyHint")}` : ""}`}
|
||||
aria-pressed={toolMode === mode}
|
||||
disabled={readOnly && mode === "connect"}
|
||||
onClick={() => {
|
||||
onToolModeChange(mode);
|
||||
onConnectClickReset();
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import type { RefObject } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import type { RefObject } from "react";
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { queryKeys } from "../../constants/queryKeys";
|
||||
import { useI18n } from "../../i18n";
|
||||
import { useToast } from "../../hooks/useToast";
|
||||
import { updateLldpCollectPolicy } from "../../services/api";
|
||||
import {
|
||||
DEFAULT_CANVAS_BG,
|
||||
|
|
@ -87,6 +88,7 @@ export type TopologyViewToolsProps = {
|
|||
|
||||
export function TopologyViewTools(props: TopologyViewToolsProps) {
|
||||
const { t } = useI18n();
|
||||
const { showError } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
fullscreen,
|
||||
|
|
@ -225,7 +227,7 @@ export function TopologyViewTools(props: TopologyViewToolsProps) {
|
|||
void queryClient.invalidateQueries({ queryKey: queryKeys.lldpCollectDashboard });
|
||||
})
|
||||
.catch(() => {
|
||||
/* local toggle still applies to this canvas discover */
|
||||
showError(t("topology.policySyncFailed"));
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useQuery, type QueryClient } from "@tanstack/react-query";
|
||||
import type { Edge, Node, ReactFlowInstance } from "@xyflow/react";
|
||||
import { fetchTopologyGraph } from "../../../services/api";
|
||||
import { fetchTopologyGraph, formatErr } from "../../../services/api";
|
||||
import { queryKeys } from "../../../constants/queryKeys";
|
||||
import type { TopologyTreeViewItem, TopologyViewGraph, TopologyWorldTransform } from "../../../types";
|
||||
import {
|
||||
|
|
@ -493,6 +493,8 @@ export function useTopologyCanvas(opts: UseTopologyCanvasOptions) {
|
|||
const showWorldScatter =
|
||||
isWorldFlatCanvas && worldVisualLod !== "full" && worldScatter.length > 0;
|
||||
const canvasGraphLoading = Boolean(mapId) && (graphQuery.isPending || !graphQuery.data);
|
||||
const canvasGraphError = Boolean(mapId) && graphQuery.isError;
|
||||
const canvasGraphErrorMsg = graphQuery.error ? formatErr(graphQuery.error) : "";
|
||||
const canvasGraphEmpty =
|
||||
Boolean(mapId) &&
|
||||
graphQuery.isSuccess &&
|
||||
|
|
@ -551,6 +553,8 @@ export function useTopologyCanvas(opts: UseTopologyCanvasOptions) {
|
|||
worldDockMe,
|
||||
showWorldScatter,
|
||||
canvasGraphLoading,
|
||||
canvasGraphError,
|
||||
canvasGraphErrorMsg,
|
||||
canvasGraphEmpty,
|
||||
worldNeedsApply,
|
||||
canvasGraphRefreshing,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
removeTopologyViewNodes,
|
||||
startLldpDiscover,
|
||||
stopLldpCollectJob,
|
||||
formatErr,
|
||||
} from "../../../services/api";
|
||||
import type {
|
||||
TopologyDiscoverJob,
|
||||
|
|
@ -181,6 +182,11 @@ export function useTopologyDiscover(opts: UseTopologyDiscoverOptions) {
|
|||
setDiscoverJobId(jobStart.id);
|
||||
let job: TopologyDiscoverJob = jobStart;
|
||||
let cancelled = false;
|
||||
const isTerminalStatus = (status: string) =>
|
||||
status === "done" ||
|
||||
status === "failed" ||
|
||||
status === "cancelled" ||
|
||||
status === "stopped";
|
||||
for (let i = 0; i < 600; i++) {
|
||||
if (discoverAbortRef.current) {
|
||||
cancelled = true;
|
||||
|
|
@ -202,12 +208,7 @@ export function useTopologyDiscover(opts: UseTopologyDiscoverOptions) {
|
|||
neIp: job.items?.[job.items.length - 1]?.ne_ip || p.neIp,
|
||||
}));
|
||||
setDiscoverLiveResults((job.items || []) as TopologyDiscoverNeResult[]);
|
||||
if (
|
||||
job.status === "done" ||
|
||||
job.status === "failed" ||
|
||||
job.status === "cancelled" ||
|
||||
job.status === "stopped"
|
||||
) {
|
||||
if (isTerminalStatus(job.status)) {
|
||||
if (job.status === "cancelled" || job.status === "stopped") cancelled = true;
|
||||
break;
|
||||
}
|
||||
|
|
@ -227,6 +228,9 @@ export function useTopologyDiscover(opts: UseTopologyDiscoverOptions) {
|
|||
});
|
||||
return;
|
||||
}
|
||||
if (!isTerminalStatus(job.status)) {
|
||||
throw new Error(t("topology.discoverTimeout"));
|
||||
}
|
||||
if (job.status === "failed") {
|
||||
throw new Error(job.error || "discover_failed");
|
||||
}
|
||||
|
|
@ -293,8 +297,9 @@ export function useTopologyDiscover(opts: UseTopologyDiscoverOptions) {
|
|||
if (discoverAbortRef.current) {
|
||||
setDiscoverError(t("topology.discoverCancelled"));
|
||||
} else {
|
||||
setDiscoverError(String(err));
|
||||
showError(t("topology.discoverFail").replace("{{detail}}", String(err)));
|
||||
const detail = formatErr(err);
|
||||
setDiscoverError(detail);
|
||||
showError(t("topology.discoverFail").replace("{{detail}}", detail));
|
||||
}
|
||||
} finally {
|
||||
setDiscovering(false);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue