mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-20 15:22:59 +00:00
feat: node-scoped Networking operator page (#1603)
* feat: add node-scoped Networking operator page Adds a Networking view with overview, topology, inventory, and findings. Shared aggregate reads back the page; Resources keeps prune and redirects here. Includes fail-closed network delete guards, operator docs, and /nodes/:slug/networking routing. * fix: rename unused variable n to _n to satisfy no-unused-vars lint * fix: keep top bar search clickable when nav grows * feat: complete Compose-first Networking Phase 2 operator assistant * fix: move networking action visibility helper out of component module * feat(networking): complete Compose-first Networking operator page Finish the node-scoped Networking page (Overview, Networks, Topology, Findings) with design-system parity and correct finding semantics. - Rebuild detail sheets on SystemSheet/SheetSection; align the tab band, masthead, and mobile tone with Fleet and Security. - Encode the host-mode and exposure severity matrix; fix collision counts so intentional shared externals are not flagged; add one typed drift predicate shared by inventory, topology, badges, and overview counts. - Preserve per-container attachments and IPs on topology node clicks; drawer-only click with an explicit logs action; ownership and boolean filters; bound large graphs before layout. - Aggregate cached Compose Doctor findings into the Findings tab with honest source labels, structural merge and dedupe, staleness reconciliation, and a shared exposure-context helper both engines use. - Networks tab: privacy-safe service search, precise ownership counts, schema v3 with version-2 adapters on every endpoint, pre-confirm delete reasons, and the shared sortable table with an internal scroll region. - Interop: Fleet node-card networking signal with pending-intent navigation, stack-to-node backlink, and Dossier/Drift deep links. - Enrich sanitized inspect with an allowlisted connected-container list; fetch topology once and filter client-side. - Docs and tests across every new finding kind, adapter, and flow. * fix(networking): correct drift count, exposure fail-soft, and inspect crash paths Address code-review findings on the Networking page implementation: - Fix the Overview drift count to use the shared drift-kind predicate instead of a hardcoded list that omitted external-network-missing. - Gate Compose Doctor's unclassified-exposure and reverse-proxy-undocumented rules on exposure-context availability, so a DB read failure no longer fabricates findings (mirrors the live engine's existing fail-soft behavior). - Guard the per-stack exposure-intent read in topology aggregation so a transient DB failure degrades to unknown intent instead of failing the whole response. - Harden the network detail drawer against a partial inspect payload from an older remote node, and log the real error instead of a bare catch. - Remove now-duplicated severity-rank and drift-kind helpers in favor of the shared modules; drop dead backend-only exports; widen the frontend schema version type to a plain number instead of casting past a literal type. - Add coverage for the delete-guard precedence, the full host-mode severity matrix, the schema-2 compatibility adapter, and the sanitized connected- container allowlist; tighten two tests that were not exercising the behavior they claimed to. * fix: add missing onOpenNodeNetworking prop to FleetView experimental test The added required prop on FleetViewProps broke the merge-build when the test file (on main but not on this branch) was compiled against the updated FleetView interface.
This commit is contained in:
@@ -72,6 +72,7 @@ const AutoUpdateReadinessView = lazy(() => import('./AutoUpdateReadinessView'));
|
||||
const AppStoreView = lazy(() => import('./AppStoreView').then(m => ({ default: m.AppStoreView })));
|
||||
const AuditLogView = lazy(() => import('./AuditLogView').then(m => ({ default: m.AuditLogView })));
|
||||
const ResourcesView = lazy(() => import('./ResourcesView'));
|
||||
const NetworkingView = lazy(() => import('./networking/NetworkingView').then(m => ({ default: m.NetworkingView })));
|
||||
const GlobalObservabilityView = lazy(() => import('./GlobalObservabilityView').then(m => ({ default: m.GlobalObservabilityView })));
|
||||
|
||||
export default function EditorLayout() {
|
||||
@@ -312,6 +313,12 @@ export default function EditorLayout() {
|
||||
pendingStackLoadRef,
|
||||
pendingLogsRef,
|
||||
} = stackActions;
|
||||
// Pending-intent target for a cross-node "open this node's Networking page"
|
||||
// request (e.g. a Fleet networking signal). Mirrors pendingStackLoadRef:
|
||||
// setActiveNode first, then the node-settled effect below navigates once
|
||||
// activeNode actually reflects the target, so Networking never briefly
|
||||
// mounts and fetches against the previous node.
|
||||
const pendingNetworkingNodeRef = useRef<number | null>(null);
|
||||
|
||||
const panelStartedAt = usePanelSessionStartedAt(panelState);
|
||||
|
||||
@@ -342,6 +349,7 @@ export default function EditorLayout() {
|
||||
// Optimistically flip to the detail surface the instant a row is tapped,
|
||||
// before loadFile's fetch resolves selectedFile; cleared once it settles.
|
||||
const [pendingDetailStack, setPendingDetailStack] = useState<string | null>(null);
|
||||
const [pendingAnatomyTab, setPendingAnatomyTab] = useState<'networking' | 'doctor' | 'dossier' | 'drift' | undefined>();
|
||||
const [fleetUpdatesIntent, setFleetUpdatesIntent] = useState<{ tab: 'nodes' | 'changelog' } | null>(null);
|
||||
|
||||
const handleFleetUpdatesIntentConsumed = useCallback(() => setFleetUpdatesIntent(null), []);
|
||||
@@ -411,6 +419,14 @@ export default function EditorLayout() {
|
||||
}
|
||||
}, [pendingDetailStack, detailReady, isFileLoading, stacksLoadStatus, urlHydratingStack, routeDetailError]);
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingAnatomyTab && selectedFile && !isFileLoading) {
|
||||
const timer = window.setTimeout(() => setPendingAnatomyTab(undefined), 0);
|
||||
return () => window.clearTimeout(timer);
|
||||
}
|
||||
return undefined;
|
||||
}, [isFileLoading, pendingAnatomyTab, selectedFile]);
|
||||
|
||||
// A phone shows one surface at a time, so every mobile navigation tears down
|
||||
// the current detail and switches surfaces, guarding a dirty editor first.
|
||||
// `then` runs the destination-specific work (navigate to a view, open
|
||||
@@ -446,9 +462,20 @@ export default function EditorLayout() {
|
||||
// is already active, else stash it and switch nodes (the node-switch effect
|
||||
// loads the pending stack once the registry settles). Mobile shows the
|
||||
// optimistic detail surface immediately.
|
||||
const handleFleetNavigateToNode = (nodeId: number, stackName: string) => {
|
||||
const handleFleetNavigateToNode = (
|
||||
nodeId: number,
|
||||
stackName: string,
|
||||
destination: SenchoOpenStackDetail['destination'] = 'stack',
|
||||
) => {
|
||||
const node = nodes.find(n => n.id === nodeId);
|
||||
if (!node) return;
|
||||
setPendingAnatomyTab(
|
||||
destination === 'anatomy-networking' ? 'networking'
|
||||
: destination === 'doctor' ? 'doctor'
|
||||
: destination === 'dossier' ? 'dossier'
|
||||
: destination === 'drift' ? 'drift'
|
||||
: undefined,
|
||||
);
|
||||
if (isMobile) setPendingDetailStack(stackName);
|
||||
if (activeNode?.id === nodeId) {
|
||||
void stackActions.loadFile(stackName);
|
||||
@@ -467,12 +494,28 @@ export default function EditorLayout() {
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
const detail = (e as CustomEvent<SenchoOpenStackDetail>).detail;
|
||||
if (detail) openStackFromEventRef.current(detail.nodeId, detail.stackName);
|
||||
if (detail) openStackFromEventRef.current(detail.nodeId, detail.stackName, detail.destination);
|
||||
};
|
||||
window.addEventListener(SENCHO_OPEN_STACK_EVENT, handler);
|
||||
return () => window.removeEventListener(SENCHO_OPEN_STACK_EVENT, handler);
|
||||
}, []);
|
||||
|
||||
// Open a node's Networking page from a Fleet card's networking signal.
|
||||
// Pending-intent gated (see pendingNetworkingNodeRef above): if the node is
|
||||
// already active, navigate immediately; otherwise switch nodes first and let
|
||||
// the node-settled effect complete the navigation once activeNode reflects
|
||||
// the switch.
|
||||
const handleOpenNodeNetworking = (nodeId: number) => {
|
||||
const node = nodes.find(n => n.id === nodeId);
|
||||
if (!node) return;
|
||||
if (activeNode?.id === nodeId) {
|
||||
setActiveView('networking');
|
||||
return;
|
||||
}
|
||||
pendingNetworkingNodeRef.current = nodeId;
|
||||
setActiveNode(node);
|
||||
};
|
||||
|
||||
// "Inspect" a node from the mobile Fleet screen: switch to it and land on its
|
||||
// stack list.
|
||||
const handleInspectNode = (nodeId: number) => {
|
||||
@@ -536,6 +579,7 @@ export default function EditorLayout() {
|
||||
const renderEditor = (headerActions?: ReactNode) => (
|
||||
<EditorView
|
||||
headerActions={headerActions}
|
||||
requestedAnatomyTab={pendingAnatomyTab}
|
||||
stackName={stackName}
|
||||
isDarkMode={isDarkMode}
|
||||
containers={containers}
|
||||
@@ -659,6 +703,8 @@ export default function EditorLayout() {
|
||||
|
||||
const pendingStack = pendingStackLoadRef.current;
|
||||
pendingStackLoadRef.current = null;
|
||||
const pendingNetworkingNodeId = pendingNetworkingNodeRef.current;
|
||||
pendingNetworkingNodeRef.current = null;
|
||||
|
||||
stackActions.resetEditorState();
|
||||
// Stack filenames can repeat across nodes; drop the previous node's failure
|
||||
@@ -667,6 +713,8 @@ export default function EditorLayout() {
|
||||
|
||||
if (pendingStack) {
|
||||
void stackActions.loadFile(pendingStack);
|
||||
} else if (pendingNetworkingNodeId === activeNode.id) {
|
||||
setActiveView('networking');
|
||||
} else if (isRealSwitch) {
|
||||
setActiveView('dashboard');
|
||||
}
|
||||
@@ -892,6 +940,7 @@ export default function EditorLayout() {
|
||||
}}
|
||||
onHostConsoleClose={() => setActiveView(selectedFile ? 'editor' : 'dashboard')}
|
||||
onFleetNavigateToNode={handleFleetNavigateToNode}
|
||||
onOpenNodeNetworking={handleOpenNodeNetworking}
|
||||
filterNodeId={filterNodeId}
|
||||
onClearScheduledOpsFilter={() => setFilterNodeId(null)}
|
||||
schedulePrefill={schedulePrefill}
|
||||
@@ -1039,6 +1088,12 @@ export default function EditorLayout() {
|
||||
<ResourcesView headerActions={mobileMastheadActions} />
|
||||
</Suspense>
|
||||
);
|
||||
case 'networking':
|
||||
return (
|
||||
<Suspense fallback={lazyFallback}>
|
||||
<NetworkingView headerActions={mobileMastheadActions} />
|
||||
</Suspense>
|
||||
);
|
||||
case 'global-observability':
|
||||
// Hub-only, like the desktop content path (ViewRouter); no capability gate.
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user