feat(app): map polish — loading spinner, cleaner cards, reveal-on-select
- Loading spinner while the graph query is in flight. - Deployment cards drop the "consumed by N" line and external-ref chips — both now live in the inspect panel, so cards are just name/port/reach. - A node's external (reference) edges are drawn only while it's selected, then removed — no permanent web of long cross-map lines. - LAN exposure edges animate like the Internet ones (animation = live traffic).
This commit is contained in:
@@ -126,11 +126,9 @@ function MapNode({ id, data }: NodeProps) {
|
|||||||
kind: string
|
kind: string
|
||||||
sub?: string
|
sub?: string
|
||||||
dim?: boolean
|
dim?: boolean
|
||||||
hub?: number
|
|
||||||
exposable?: boolean
|
exposable?: boolean
|
||||||
program?: string | null
|
program?: string | null
|
||||||
reach?: string | null
|
reach?: string | null
|
||||||
externals?: { name: string; host: string; protocol: string }[]
|
|
||||||
focusDim?: boolean
|
focusDim?: boolean
|
||||||
focused?: boolean
|
focused?: boolean
|
||||||
onMenu?: (x: number, y: number, name: string, kind: string) => void
|
onMenu?: (x: number, y: number, name: string, kind: string) => void
|
||||||
@@ -197,25 +195,6 @@ function MapNode({ id, data }: NodeProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{d.sub && <div className="truncate text-[10px] text-[var(--muted)]">{d.sub}</div>}
|
{d.sub && <div className="truncate text-[10px] text-[var(--muted)]">{d.sub}</div>}
|
||||||
{typeof d.hub === "number" && d.hub > 0 && (
|
|
||||||
<div className="text-[10px]" style={{ color }}>
|
|
||||||
← consumed by {d.hub}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{d.externals && d.externals.length > 0 && (
|
|
||||||
<div className="mt-0.5 flex flex-wrap gap-0.5">
|
|
||||||
{d.externals.map((x) => (
|
|
||||||
<span
|
|
||||||
key={x.name}
|
|
||||||
className="inline-flex items-center gap-0.5 rounded border border-[var(--border)] bg-black/20 px-1 text-[9px] text-[var(--muted)]"
|
|
||||||
title={`consumes external: ${x.name} (${x.protocol})`}
|
|
||||||
>
|
|
||||||
<ExternalLink size={8} />
|
|
||||||
{x.host}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{/* Right handle: drag OUT to connect — to a hub (expose) or another node
|
{/* Right handle: drag OUT to connect — to a hub (expose) or another node
|
||||||
(declare "requires"). Right target lands incoming dependency lines. */}
|
(declare "requires"). Right target lands incoming dependency lines. */}
|
||||||
@@ -373,7 +352,7 @@ interface MenuItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SystemMapPage() {
|
export function SystemMapPage() {
|
||||||
const { data: graph } = useGraph()
|
const { data: graph, isLoading: graphLoading } = useGraph()
|
||||||
const { data: jobs } = useJobs()
|
const { data: jobs } = useJobs()
|
||||||
const { data: programs } = usePrograms()
|
const { data: programs } = usePrograms()
|
||||||
const { data: gateway } = useGateway()
|
const { data: gateway } = useGateway()
|
||||||
@@ -436,16 +415,6 @@ export function SystemMapPage() {
|
|||||||
|
|
||||||
// Consumption of an external `reference` renders as a chip on the consumer (not
|
// Consumption of an external `reference` renders as a chip on the consumer (not
|
||||||
// a long cross-map edge). Collect them here, keyed by the consuming deployment.
|
// a long cross-map edge). Collect them here, keyed by the consuming deployment.
|
||||||
const externalsOf = new Map<string, { name: string; host: string; protocol: string }[]>()
|
|
||||||
for (const e of graph.edges) {
|
|
||||||
if (e.kind !== "deployment") continue
|
|
||||||
const dst = byName.get(e.dst)
|
|
||||||
if (dst?.kind !== "reference") continue
|
|
||||||
const arr = externalsOf.get(e.src) ?? []
|
|
||||||
arr.push({ name: e.dst, host: hostOf(dst.base_url) || e.dst, protocol: protoOf(dst.base_url) })
|
|
||||||
externalsOf.set(e.src, arr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// A job shows its cron; everything else prefers its declared socket, else its
|
// A job shows its cron; everything else prefers its declared socket, else its
|
||||||
// gateway http port. This is what makes raw-TCP infra (postgres :5432) visible.
|
// gateway http port. This is what makes raw-TCP infra (postgres :5432) visible.
|
||||||
const subFor = (n: (typeof graph.nodes)[number]): string | undefined => {
|
const subFor = (n: (typeof graph.nodes)[number]): string | undefined => {
|
||||||
@@ -473,11 +442,9 @@ export function SystemMapPage() {
|
|||||||
kind: n.kind,
|
kind: n.kind,
|
||||||
sub: subFor(n),
|
sub: subFor(n),
|
||||||
dim: lane.dim,
|
dim: lane.dim,
|
||||||
hub: n.depended_on_by,
|
|
||||||
exposable: !!lane.exposable,
|
exposable: !!lane.exposable,
|
||||||
program: n.program && catalog.has(n.program) ? n.program : null,
|
program: n.program && catalog.has(n.program) ? n.program : null,
|
||||||
reach: n.reach,
|
reach: n.reach,
|
||||||
externals: externalsOf.get(n.name),
|
|
||||||
},
|
},
|
||||||
} satisfies Node
|
} satisfies Node
|
||||||
})
|
})
|
||||||
@@ -664,7 +631,7 @@ export function SystemMapPage() {
|
|||||||
target: isPub ? "__internet__" : "__gateway__",
|
target: isPub ? "__internet__" : "__gateway__",
|
||||||
sourceHandle: "rs",
|
sourceHandle: "rs",
|
||||||
targetHandle: "lt",
|
targetHandle: "lt",
|
||||||
animated: isPub,
|
animated: true,
|
||||||
deletable: true,
|
deletable: true,
|
||||||
style: { stroke: isPub ? "#2ea043" : "#58a6ff", strokeWidth: 1.75 },
|
style: { stroke: isPub ? "#2ea043" : "#58a6ff", strokeWidth: 1.75 },
|
||||||
})
|
})
|
||||||
@@ -745,8 +712,31 @@ export function SystemMapPage() {
|
|||||||
reachRef.current = built.reachOf
|
reachRef.current = built.reachOf
|
||||||
focusRef.current = focus
|
focusRef.current = focus
|
||||||
setNodes(materialize(built))
|
setNodes(materialize(built))
|
||||||
setEdges(dimEdges(built.edges))
|
// External consumption is drawn only while the consumer is selected — a dashed
|
||||||
}, [built, focus, setNodes, setEdges, materialize, dimEdges])
|
// line to each of its reference nodes (kept off-canvas otherwise to avoid a
|
||||||
|
// permanent web of long cross-map lines).
|
||||||
|
const extEdges: Edge[] = []
|
||||||
|
if (focus && graph) {
|
||||||
|
const refNames = new Set(
|
||||||
|
graph.nodes.filter((n) => n.kind === "reference").map((n) => n.name),
|
||||||
|
)
|
||||||
|
for (const e of graph.edges) {
|
||||||
|
if (e.kind === "deployment" && e.src === focus && refNames.has(e.dst)) {
|
||||||
|
extEdges.push({
|
||||||
|
id: `ext:${focus}->${e.dst}`,
|
||||||
|
source: focus,
|
||||||
|
target: e.dst,
|
||||||
|
sourceHandle: "rs",
|
||||||
|
targetHandle: "lt",
|
||||||
|
animated: true,
|
||||||
|
deletable: false,
|
||||||
|
style: { stroke: "#8b949e", strokeWidth: 1.5, strokeDasharray: "4 3" },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setEdges([...dimEdges(built.edges), ...extEdges])
|
||||||
|
}, [built, focus, graph, setNodes, setEdges, materialize, dimEdges])
|
||||||
|
|
||||||
// Single-node selection = inspect (dim + panel); 0 or many = no focus (drag mode).
|
// Single-node selection = inspect (dim + panel); 0 or many = no focus (drag mode).
|
||||||
const onSelectionChange = useCallback((p: { nodes: Node[] }) => {
|
const onSelectionChange = useCallback((p: { nodes: Node[] }) => {
|
||||||
@@ -1060,6 +1050,12 @@ export function SystemMapPage() {
|
|||||||
colorMode="dark"
|
colorMode="dark"
|
||||||
>
|
>
|
||||||
<Background color="#30363d" gap={20} />
|
<Background color="#30363d" gap={20} />
|
||||||
|
{(graphLoading || !graph) && (
|
||||||
|
<div className="absolute inset-0 z-20 flex items-center justify-center gap-2 text-sm text-[var(--muted)]">
|
||||||
|
<Loader2 size={18} className="animate-spin" />
|
||||||
|
Loading map…
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Controls showInteractive={false}>
|
<Controls showInteractive={false}>
|
||||||
<ControlButton
|
<ControlButton
|
||||||
onClick={() => setLasso((v) => !v)}
|
onClick={() => setLasso((v) => !v)}
|
||||||
|
|||||||
Reference in New Issue
Block a user