shell-redesign V1 polish slice Phase 5.1 P5.1-5: pod-scope graph view

V1 founder-walk surfaced a missing graph view at the pod scope
(/topology/pod/$rigId/$podName previously rendered a placeholder
without the live graph render). Per Phase 5.1 ACK §6 implementation
plan, pod-scope graph was the "investigate; park if new-build" item.
Investigation showed it can ride on the existing RigGraph data
(single rig) with a small filter prop — NOT new build. Shipping inline.

EXTENDED packages/ui/src/components/RigGraph.tsx:
- New optional `podScope?: string` prop. When set, the rawNodes /
  rawEdges streams from useRigGraph(rigId) are filtered to:
  * podGroup/group container nodes whose data.podNamespace OR data.podId
    matches the pod name
  * agent nodes whose data.podNamespace / data.podId / inferPodName
    falls in the pod
  * edges whose source AND target are both kept
- Cast inline to RigNodeShape / RigEdgeShape since useRigGraph's hook
  data is typed as unknown[] (matching existing downstream pattern).

REFACTORED packages/ui/src/components/topology/ScopePages.tsx:
- PodScopePage default tab moved from "table" → "graph" so the new
  graph view-mode is the landing surface.
- Graph view-mode: <RigGraph rigId={rigId} podScope={podName} ... />.
  Pod-scoped subset renders with same paper-grid + tactical aesthetic.
- Mobile graph→table degradation pattern replicated from HostScopePage
  + RigScopePage (P5-9): at <lg viewport, graph effectively renders
  table with degraded hint; useShellViewport gate.
- Table + Terminal + Overview view-modes unchanged.

Verification:
- pnpm -F @openrig/ui exec tsc --noEmit: clean
- vitest rig-graph.test.tsx: 61/61 PASS (existing tests unaffected by
  podScope addition since tests don't pass the prop)

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
mvschwarzandClaude Opus 4.7 committed 2026-05-06 20:11:36 -07:00
1 parent 1eb71fc0b8
commit 971170daf0
2 files changed
+64 -8

No files matched your search

+42 -2
View File
@@ -96,15 +96,55 @@ export function RigGraph({
rigId,
rigName = null,
showDiscovered = true,
podScope,
}: {
rigId: string | null;
rigName?: string | null;
showDiscovered?: boolean;
/** V1 polish slice Phase 5.1 P5.1-5: pod-scope filter. When set, the
* graph renders only nodes/edges/podGroups whose pod matches this
* name (matched via inferPodName + node.podId/podNamespace). Other
* rig nodes are filtered out so the graph reads as a single-pod
* subset. Used by /topology/pod/$rigId/$podName graph view-mode. */
podScope?: string;
}) {
const { data, isPending: loading, error: queryError } = useRigGraph(rigId ?? "");
const discoveredSessions = useDiscoveredSessionsConditional(showDiscovered);
const rawNodes = data?.nodes ?? [];
const rawEdges = data?.edges ?? [];
const allRawNodes = data?.nodes ?? [];
const allRawEdges = data?.edges ?? [];
// P5.1-5 pod-scope filter: when podScope set, restrict nodes to those
// whose pod matches; restrict edges to those between filtered nodes.
// Hook data is typed as unknown[]; cast inline to known shape.
const { rawNodes, rawEdges } = useMemo(() => {
if (!podScope) return { rawNodes: allRawNodes, rawEdges: allRawEdges };
type RigNodeShape = {
id: string;
type?: string;
data?: { logicalId?: string; podId?: string | null; podNamespace?: string | null };
};
type RigEdgeShape = { source: string; target: string };
const allowedNodeIds = new Set<string>();
const filteredNodes = (allRawNodes as RigNodeShape[]).filter((n) => {
if (n.type === "podGroup" || n.type === "group") {
const matches =
(n.data?.podNamespace ?? n.data?.podId) === podScope;
if (matches) allowedNodeIds.add(n.id);
return matches;
}
const inferredPod =
n.data?.podNamespace ??
n.data?.podId ??
inferPodName(n.data?.logicalId ?? null);
const matches = inferredPod === podScope;
if (matches) allowedNodeIds.add(n.id);
return matches;
});
const filteredEdges = (allRawEdges as RigEdgeShape[]).filter(
(e) => allowedNodeIds.has(e.source) && allowedNodeIds.has(e.target),
);
return { rawNodes: filteredNodes, rawEdges: filteredEdges };
}, [allRawNodes, allRawEdges, podScope]);
const error = queryError?.message ?? null;
const { reconnecting } = useRigEvents(rigId);
const edgeActivity = useTopologyEdgeActivity();
@@ -253,9 +253,17 @@ function RigOverviewTab({ rigId, rigName }: { rigId: string; rigName: string | n
}
export function PodScopePage() {
// V1 polish slice Phase 5.1 P5.1-5: pod-scope graph wires through
// RigGraph's new podScope prop (filters nodes + edges + pod groups
// to the matching pod only). Default tab moved to "graph" so the
// graph view-mode is the landing surface (matches host/rig scope
// pattern; founder direction "I noticed that there's no graph view
// when you click on a pod").
const { rigId, podName } = useParams({ from: "/topology/pod/$rigId/$podName" });
const [active, setActive] = useState<TopologyRigPodScopeTab>("table");
const [active, setActive] = useState<TopologyRigPodScopeTab>("graph");
const { isWideLayout } = useShellViewport();
useOverlayForActiveTab(active);
const effectiveActive = !isWideLayout && active === "graph" ? "table" : active;
return (
<ScopeShell
@@ -263,17 +271,25 @@ export function PodScopePage() {
title={`${rigId} / ${podName}`}
tabsNav={<TopologyViewModeTabs tabs={RIG_POD_SCOPE_TABS} active={active} onSelect={setActive} testIdPrefix="topology-pod" />}
>
{active === "graph" ? (
<div className="p-6">
<EmptyState label="POD GRAPH" description="Pod-scoped graph view (Phase 5 polish)." variant="card" />
{effectiveActive === "graph" ? (
<div className="flex-1 min-h-0 relative">
<RigGraph rigId={rigId} rigName={null} showDiscovered={false} podScope={podName} />
</div>
) : null}
{active === "table" ? (
{effectiveActive === "table" ? (
<div className="px-6 pb-6">
{!isWideLayout && active === "graph" ? (
<p
data-testid="topology-mobile-graph-degraded"
className="font-mono text-[9px] text-on-surface-variant italic mb-2"
>
Graph view degrades to table on narrow viewports.
</p>
) : null}
<TopologyTableView rigIdScope={rigId} />
</div>
) : null}
{active === "terminal" ? (
{effectiveActive === "terminal" ? (
<TopologyTerminalView scope="pod" rigId={rigId} podName={podName} />
) : null}
{active === "overview" ? (