import React, { useMemo, useState } from 'react'; import { ArrowLeft, ArrowRight, Crosshair, Globe, GitMerge, MousePointer2, Plus, Server, Trash2, X, Shield, } from '../../icons'; import type { Net, MazeNode, Edge } from './types'; import { DEFAULT_SERVICES } from './data'; export type Selection = | { type: 'net'; id: string } | { type: 'node'; id: string } | { type: 'edge'; id: string } | { type: 'service'; id: string; nodeId: string } | null; interface Props { selection: Selection; nets: Net[]; nodes: MazeNode[]; edges: Edge[]; topologyStatus?: string; onClose?: () => void; onDeleteNet?: (id: string) => void; onDeleteNode?: (id: string) => void; onDeleteEdge?: (id: string) => void; onRemoveService?: (nodeId: string, slug: string) => void; // Live (post-deploy) service mutation, hitting W3 endpoints directly. // Distinct from onRemoveService which queues a design-time graph // mutation. Both can coexist; the inspector picks based on // topologyStatus (active/degraded → live, pending/anything else → // design-time only). Wiring these props from MazeNET.tsx is the // single switch that turns chips into live controls. onLiveAddService?: (nodeName: string, slug: string) => Promise; onLiveRemoveService?: (nodeName: string, slug: string) => Promise; /** Per-decky-eligible service slugs, fetched via useServiceRegistry. */ availableServices?: string[]; /** Toggle ``forwards_l3`` (gateway) on the selected decky. When the * topology is active/degraded the caller is responsible for the * destructive-recreate confirm dialog and the ``force: true`` submit * — this prop just relays the user's intent. */ onToggleGateway?: (nodeId: string, nextValue: boolean) => Promise; onAddDecky?: (netId: string) => void; setSelection?: (sel: Selection) => void; pendingChanges?: number; className?: string; } const Inspector: React.FC = ({ selection, nets, nodes, edges, topologyStatus, onClose, onDeleteNet, onDeleteNode, onDeleteEdge, onRemoveService, onLiveAddService, onLiveRemoveService, availableServices = [], onToggleGateway, onAddDecky, setSelection, pendingChanges = 0, className = '', }) => { const liveOpsEnabled = !!onLiveAddService && !!onLiveRemoveService && (topologyStatus === 'active' || topologyStatus === 'degraded'); const [addOpen, setAddOpen] = useState(false); const [addSlug, setAddSlug] = useState(''); const [busy, setBusy] = useState(null); // slug currently mutating const [opError, setOpError] = useState(null); const net = selection?.type === 'net' ? nets.find((n) => n.id === selection.id) : undefined; const node = selection?.type === 'node' ? nodes.find((n) => n.id === selection.id) : undefined; const edge = selection?.type === 'edge' ? edges.find((e) => e.id === selection.id) : undefined; const serviceSel = selection?.type === 'service' ? selection : undefined; const serviceMeta = serviceSel ? DEFAULT_SERVICES.find((s) => s.slug === serviceSel.id) : undefined; const serviceParent = serviceSel ? nodes.find((n) => n.id === serviceSel.nodeId) : undefined; const serviceParentNet = serviceParent ? nets.find((n) => n.id === serviceParent.netId) : undefined; const activeNetIds = useMemo(() => { const s = new Set(); edges.forEach((e) => { const f = nodes.find((n) => n.id === e.from); const t = nodes.find((n) => n.id === e.to); if (f) s.add(f.netId); if (t) s.add(t.netId); }); return s; }, [edges, nodes]); const typeLabel = selection ? selection.type.toUpperCase() : 'IDLE'; const isGateway = node?.kind === 'decky' && !!node.decky_config?.forwards_l3; const isObserved = node?.kind === 'observed'; return ( ); }; export default Inspector;