mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-25 17:57:06 +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:
@@ -37,6 +37,10 @@ import { getNodeCpu, getNodeMem, getNodeDisk, isCritical } from './nodeUtils';
|
||||
export interface NodeCardProps {
|
||||
node: FleetNode;
|
||||
onNavigate: (nodeId: number, stackName: string) => void;
|
||||
/** Switches to this node and opens its Networking page. */
|
||||
onOpenNetworking?: (nodeId: number) => void;
|
||||
/** Networking posture signal from computeNodeNetworkingSummary, if loaded. */
|
||||
networkingSignal?: { exposed: boolean; unknown: boolean; drift: boolean };
|
||||
labelMap?: Record<string, StackLabel[]>;
|
||||
updateStatus?: NodeUpdateStatus;
|
||||
onUpdate?: (nodeId: number) => void;
|
||||
@@ -64,7 +68,7 @@ function UsageBar({ percent, color }: { percent: number; color: string }) {
|
||||
|
||||
// --- Main Export ---
|
||||
|
||||
export function NodeCard({ node, onNavigate, labelMap, updateStatus, onUpdate, updatingNodeId, onRetryUpdate, onDismissUpdate, onCordonChange, onEdit, onDelete, onOpenMuteRulesWithPrefill }: NodeCardProps) {
|
||||
export function NodeCard({ node, onNavigate, onOpenNetworking, networkingSignal, labelMap, updateStatus, onUpdate, updatingNodeId, onRetryUpdate, onDismissUpdate, onCordonChange, onEdit, onDelete, onOpenMuteRulesWithPrefill }: NodeCardProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [stacks, setStacks] = useState<string[] | null>(node.stacks);
|
||||
const [loadingStacks, setLoadingStacks] = useState(false);
|
||||
@@ -255,6 +259,17 @@ export function NodeCard({ node, onNavigate, labelMap, updateStatus, onUpdate, u
|
||||
<Ban className="w-2.5 h-2.5 mr-0.5" /> Cordoned
|
||||
</Badge>
|
||||
)}
|
||||
{onOpenNetworking && networkingSignal && (networkingSignal.exposed || networkingSignal.unknown || networkingSignal.drift) && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[10px] px-1.5 py-0 h-4 shrink-0 cursor-pointer bg-warning/10 text-warning border-warning/30 hover:bg-warning/20"
|
||||
onClick={(event) => { event.stopPropagation(); onOpenNetworking(node.id); }}
|
||||
title="Open this node's Networking page"
|
||||
>
|
||||
Networking ·
|
||||
{networkingSignal.drift ? ' drift' : networkingSignal.exposed ? ' exposed' : ' unknown exposure'}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,6 +29,8 @@ interface OverviewTabProps {
|
||||
fleetStackLabelMap: Record<number, Record<string, StackLabel[]>>;
|
||||
updateStatusMap: Map<number, NodeUpdateStatus>;
|
||||
onNavigateToNode: (nodeId: number, stackName: string) => void;
|
||||
onOpenNodeNetworking: (nodeId: number) => void;
|
||||
networkingByNode: Map<number, { exposed: boolean; unknown: boolean; drift: boolean }>;
|
||||
onUpdate?: (nodeId: number) => void;
|
||||
updatingNodeId: number | null;
|
||||
onRetryUpdate?: (nodeId: number) => void;
|
||||
@@ -65,6 +67,8 @@ export function OverviewTab({
|
||||
fleetStackLabelMap,
|
||||
updateStatusMap,
|
||||
onNavigateToNode,
|
||||
onOpenNodeNetworking,
|
||||
networkingByNode,
|
||||
onUpdate,
|
||||
updatingNodeId,
|
||||
onRetryUpdate,
|
||||
@@ -143,6 +147,8 @@ export function OverviewTab({
|
||||
key={node.id}
|
||||
node={node}
|
||||
onNavigate={onNavigateToNode}
|
||||
onOpenNetworking={onOpenNodeNetworking}
|
||||
networkingSignal={networkingByNode.get(node.id)}
|
||||
labelMap={fleetStackLabelMap[node.id] ?? {}}
|
||||
updateStatus={updateStatusMap.get(node.id)}
|
||||
onUpdate={onUpdate}
|
||||
|
||||
@@ -128,4 +128,31 @@ describe('NodeCard', () => {
|
||||
expect(screen.getByText('Pinned')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: /Update/ })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the networking signal badge and switches to the node on click', async () => {
|
||||
const onOpenNetworking = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<NodeCard
|
||||
{...baseProps(onlineNode())}
|
||||
onOpenNetworking={onOpenNetworking}
|
||||
networkingSignal={{ exposed: false, unknown: false, drift: true }}
|
||||
/>,
|
||||
);
|
||||
const badge = screen.getByText(/Networking/);
|
||||
expect(badge).toBeInTheDocument();
|
||||
await user.click(badge);
|
||||
expect(onOpenNetworking).toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
it('hides the networking signal badge when there is nothing to flag', () => {
|
||||
render(
|
||||
<NodeCard
|
||||
{...baseProps(onlineNode())}
|
||||
onOpenNetworking={vi.fn()}
|
||||
networkingSignal={{ exposed: false, unknown: false, drift: false }}
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByText(/Networking/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,6 +35,8 @@ function props(overrides: Partial<React.ComponentProps<typeof OverviewTab>> = {}
|
||||
fleetStackLabelMap: {},
|
||||
updateStatusMap: new Map(),
|
||||
onNavigateToNode: vi.fn(),
|
||||
onOpenNodeNetworking: vi.fn(),
|
||||
networkingByNode: new Map(),
|
||||
updatingNodeId: null,
|
||||
topologyMode: 'hub' as const,
|
||||
onTopologyModeChange: vi.fn(),
|
||||
|
||||
@@ -255,5 +255,6 @@ export function useFleetOverview({ prefs, updatePrefs, updateStatuses }: UseFlee
|
||||
distinctNodeLabels: distinctLabels,
|
||||
activeFilterCount,
|
||||
clearFilters,
|
||||
networkingByNode,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user