@fougere/admin 0.9.2-alpha.0 → 0.9.3-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"topology-page.d.ts","sourceRoot":"","sources":["../src/topology-page.tsx"],"names":[],"mappings":"AAeA,OAAO,EAAuB,KAAK,YAAY,EAAE,MAAM,OAAO,CAAC;AAE/D,OAAO,EAA4C,KAAK,OAAO,EAAE,MAAM,qBAAqB,CAAC;AA6E7F,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,eAAe,CAAC,EAAE,QAAwB,EAAE,OAAwB,EAAE,GAAE,oBAAyB,GAAG,YAAY,CA6F/H"}
1
+ {"version":3,"file":"topology-page.d.ts","sourceRoot":"","sources":["../src/topology-page.tsx"],"names":[],"mappings":"AAiBA,OAAO,EAAgC,KAAK,YAAY,EAAE,MAAM,OAAO,CAAC;AAExE,OAAO,EAA4C,KAAK,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAkP7F,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,eAAe,CAAC,EAAE,QAAwB,EAAE,OAAwB,EAAE,GAAE,oBAAyB,GAAG,YAAY,CA8G/H"}
@@ -1,22 +1,24 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  /**
4
4
  * The shape of the system, drawn from what the app says about itself.
5
5
  *
6
6
  * Every other page in this panel renders a DOOR — a list, a form, a row. This one renders
7
7
  * the app: which fronds run in the process it is talking to, which answered from somewhere
8
- * else, and who called whom. Nothing here is configured, and nothing is declared: a frond
9
- * is remote because it answered a call nobody hosts.
8
+ * else, who called whom — and which the config names but nothing has ever answered from.
9
+ * The first three are OBSERVED: a frond is elsewhere because it answered, never because
10
+ * `remotes:` said so. The fourth is the only declared one, and it is the only one the
11
+ * observed half cannot hold — a frond that never answered is absent from it.
10
12
  *
11
13
  * It is served by `@fougere/observability`, so its absence is a legible state and not an
12
14
  * error — which is why the entry stays in the menu when nothing answers. Hiding it would
13
15
  * hide the one place that can say what is missing.
14
16
  */
15
17
  import { Box, Card, CardContent, Chip, Skeleton, Typography } from '@mui/material';
16
- import { useEffect, useState } from 'react';
18
+ import { useEffect, useMemo, useState } from 'react';
17
19
  import { Title, useTranslate } from 'react-admin';
18
20
  import { CALL_ENDPOINT, fetcher as browserFetcher } from '@fougere/app/client';
19
- import { fetchTopology, isOpaque, nodesOf } from './topology.js';
21
+ import { fetchTopology, figureOf, isOpaque, layoutOf, nodesOf } from './topology.js';
20
22
  /**
21
23
  * Every sentence this page draws goes through a key. Nothing under `@fougere/admin` is
22
24
  * written in a language — the counts below carry `smart_count`, which is what lets a
@@ -32,17 +34,102 @@ function EdgeLine({ edge, direction }) {
32
34
  const label = useLabels();
33
35
  return (_jsxs(Row, { children: [_jsx(Typography, { variant: "caption", sx: { fontFamily: 'monospace' }, children: direction === 'out' ? `→ ${edge.to}` : `← ${edge.from}` }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: edge.count }), edge.errors > 0 && (_jsx(Chip, { size: "small", color: "error", label: label('topology.refused', `${edge.errors} refused`, { smart_count: edge.errors }) }))] }));
34
36
  }
37
+ /**
38
+ * A palette path as SVG paint. `sx` resolves `'error.main'` for `color` and `bgcolor` and for
39
+ * nothing else, so `fill` and `stroke` reach CSS verbatim — an invalid value, and SVG falls back
40
+ * to BLACK. Every shape on this page is painted through here.
41
+ */
42
+ function paint(paths) {
43
+ const read = (theme, path) => path.split('.').reduce((held, key) => held?.[key], theme.palette);
44
+ return (theme) => ({
45
+ ...(paths.fill ? { fill: read(theme, paths.fill) } : {}),
46
+ ...(paths.stroke ? { stroke: read(theme, paths.stroke) } : {}),
47
+ });
48
+ }
49
+ /** What a node is painted with, by the one thing that separates the three states. */
50
+ function toneOf(node) {
51
+ if (node.silent)
52
+ return 'warning';
53
+ return node.placement === 'local' ? 'primary' : 'grey';
54
+ }
55
+ /**
56
+ * The system, drawn. Placement is `layoutOf`'s — ranks, order and routing.
57
+ *
58
+ * The nodes are HTML over an SVG that draws only the lines, which is how React Flow is built and
59
+ * why its nodes look like the rest of an app: a card is a `Box` with `sx`, so it reads the theme
60
+ * like every other element. An SVG `<text>` positioned by hand does not.
61
+ *
62
+ * Both layers sit on ONE scale — the container takes the drawing's aspect ratio, and every node is
63
+ * sized AND placed in per cent of it. A size in pixels does not grow with the viewBox the SVG
64
+ * scales to its container, and the lines then stop short of the cards they point at.
65
+ *
66
+ * What the drawing says beyond who reaches whom: thickness is volume, a broken line is declared
67
+ * and never travelled, and refusals are a figure rather than a red line — colour alone excludes a
68
+ * reader who cannot see it, and "how many" is the question a red line raises without answering.
69
+ */
70
+ function Graph({ nodes, selected, onSelect }) {
71
+ const label = useLabels();
72
+ const drawing = useMemo(() => layoutOf(nodes), [nodes]);
73
+ const per = (value, of) => `${(value / of) * 100}%`;
74
+ return (_jsxs(Card, { variant: "outlined", children: [_jsxs(Box, { sx: { position: 'relative', bgcolor: 'action.hover', aspectRatio: `${drawing.width} / ${drawing.height}` }, children: [_jsxs(Box, { component: "svg", viewBox: `0 0 ${drawing.width} ${drawing.height}`, sx: { position: 'absolute', inset: 0, width: '100%', height: '100%' }, children: [_jsx("defs", { children: _jsx("marker", { id: "fougere-arrow", viewBox: "0 0 8 8", refX: "6.5", refY: "4", markerWidth: "5", markerHeight: "5", orient: "auto-start-reverse", children: _jsx(Box, { component: "path", d: "M 0 0 L 8 4 L 0 8 z", sx: paint({ fill: 'text.disabled' }) }) }) }), drawing.edges.map((edge) => (_jsx(Box, { component: "path", d: edge.path, fill: "none", strokeLinecap: "round", strokeWidth: edge.weight === undefined ? 1.5 : 1.5 + edge.weight * 3, strokeDasharray: edge.weight === undefined ? '2 5' : undefined, markerEnd: "url(#fougere-arrow)", sx: [paint({ stroke: 'text.disabled' }), { strokeOpacity: 0.5 }] }, `${edge.from}-${edge.to}`)))] }), drawing.edges.map((edge) => edge.at && (_jsx(Box, { sx: {
75
+ position: 'absolute',
76
+ left: per(edge.at.x, drawing.width),
77
+ top: per(edge.at.y, drawing.height),
78
+ transform: 'translate(-50%, -50%)',
79
+ px: 0.75,
80
+ borderRadius: 1,
81
+ border: 1,
82
+ borderColor: 'divider',
83
+ bgcolor: 'background.paper',
84
+ fontFamily: 'monospace',
85
+ fontSize: 11,
86
+ fontWeight: 600,
87
+ lineHeight: '17px',
88
+ whiteSpace: 'nowrap',
89
+ color: edge.errors > 0 ? 'error.main' : 'text.secondary',
90
+ }, children: edge.count === undefined ? label('topology.noTraffic', 'no traffic') : figureOf(edge) }, `figure-${edge.from}-${edge.to}`))), drawing.nodes.map(({ node, x, y, width, height }) => {
91
+ const tone = toneOf(node);
92
+ return (_jsxs(Box, { onClick: () => onSelect(node.frond), sx: {
93
+ position: 'absolute',
94
+ left: per(x, drawing.width),
95
+ top: per(y, drawing.height),
96
+ width: per(width, drawing.width),
97
+ height: per(height, drawing.height),
98
+ transform: 'translate(-50%, -50%)',
99
+ display: 'flex',
100
+ flexDirection: 'column',
101
+ justifyContent: 'center',
102
+ gap: 0.25,
103
+ overflow: 'hidden',
104
+ px: 1.25,
105
+ borderRadius: 1.5,
106
+ border: node.frond === selected ? 2 : 1.4,
107
+ borderStyle: node.silent ? 'dashed' : 'solid',
108
+ borderColor: tone === 'grey' ? 'divider' : `${tone}.main`,
109
+ bgcolor: 'background.paper',
110
+ cursor: 'pointer',
111
+ }, children: [_jsx(Box, { sx: { fontFamily: 'monospace', fontSize: 13.5, fontWeight: 600 }, children: node.frond }), _jsxs(Row, { children: [!node.silent && (_jsx(Typography, { variant: "caption", color: "text.secondary", sx: { fontSize: 10.5 }, children: isOpaque(node)
112
+ ? label('topology.elsewhere', 'elsewhere')
113
+ : `${node.entities} · ${node.doors}` })), tone !== 'grey' && (_jsx(Chip, { size: "small", color: tone, variant: "outlined", sx: { height: 16, '& .MuiChip-label': { px: 0.75, fontSize: 9.5, letterSpacing: '0.05em' } }, label: node.silent ? label('topology.unheard', 'unheard') : label('topology.here', 'here') }))] })] }, node.frond));
114
+ })] }), _jsx(Box, { sx: { px: 2, py: 1.25 }, children: _jsx(Typography, { variant: "caption", color: "text.secondary", children: label('topology.legend', 'Thickness is volume. A broken line is declared and never travelled. Click a frond for what it holds.') }) })] }));
115
+ }
35
116
  function FrondCard({ node }) {
36
117
  const label = useLabels();
37
118
  const here = node.placement === 'local';
38
- return (_jsx(Card, { variant: "outlined", children: _jsxs(CardContent, { sx: { display: 'grid', gap: 1 }, children: [_jsxs(Row, { children: [_jsx(Typography, { variant: "h6", sx: { fontWeight: 700 }, children: node.frond }), _jsx(Chip, { size: "small", color: here ? 'primary' : 'default', label: here ? label('topology.here', 'here') : label('topology.elsewhere', 'elsewhere') })] }), _jsx(Typography, { variant: "body2", color: "text.secondary", children: isOpaque(node)
39
- ? label('topology.opaque', 'Its shape is published by the process that owns it.')
40
- : `${label('topology.entities', `${node.entities} entities`, { smart_count: node.entities })}`
41
- + ` · ${label('topology.doors', `${node.doors} doors`, { smart_count: node.doors })}` }), node.calls.map((edge) => _jsx(EdgeLine, { edge: edge, direction: "out" }, `out-${edge.to}`)), node.calledBy.map((edge) => _jsx(EdgeLine, { edge: edge, direction: "in" }, `in-${edge.from}`))] }) }));
119
+ return (_jsx(Card, { variant: "outlined", children: _jsxs(CardContent, { sx: { display: 'grid', gap: 1 }, children: [_jsxs(Row, { children: [_jsx(Typography, { variant: "h6", sx: { fontWeight: 700 }, children: node.frond }), _jsx(Chip, { size: "small", color: node.silent ? 'warning' : here ? 'primary' : 'default', label: node.silent
120
+ ? label('topology.unheard', 'declared, unheard')
121
+ : here ? label('topology.here', 'here') : label('topology.elsewhere', 'elsewhere') })] }), _jsx(Typography, { variant: "body2", color: "text.secondary", children: node.silent
122
+ ? label('topology.silent', `Declared at ${node.at ?? 'no address'} — nothing has answered from it.`, { at: node.at ?? '' })
123
+ : isOpaque(node)
124
+ ? label('topology.opaque', 'Its shape is published by the process that owns it.')
125
+ : `${label('topology.entities', `${node.entities} entities`, { smart_count: node.entities })}`
126
+ + ` · ${label('topology.doors', `${node.doors} doors`, { smart_count: node.doors })}` }), node.calls.map((edge) => _jsx(EdgeLine, { edge: edge, direction: "out" }, `out-${edge.to}`)), node.calledBy.map((edge) => _jsx(EdgeLine, { edge: edge, direction: "in" }, `in-${edge.from}`))] }) }));
42
127
  }
43
128
  export function FougereTopology({ endpoint = CALL_ENDPOINT, fetcher = browserFetcher } = {}) {
44
129
  const label = useLabels();
45
130
  const [state, setState] = useState({ status: 'loading' });
131
+ /** Which frond the detail below is about — the drawing answers "where", one card answers "what". */
132
+ const [selected, setSelected] = useState();
46
133
  useEffect(() => {
47
134
  let live = true;
48
135
  fetchTopology(endpoint, fetcher)
@@ -63,7 +150,14 @@ export function FougereTopology({ endpoint = CALL_ENDPOINT, fetcher = browserFet
63
150
  return (_jsxs(Box, { sx: { p: 3, maxWidth: 620 }, children: [title, _jsx(Card, { children: _jsxs(CardContent, { sx: { display: 'grid', gap: 1.5 }, children: [_jsx(Typography, { variant: "h6", children: label('topology.failedTitle', 'The app did not answer') }), _jsx(Typography, { component: "pre", variant: "caption", sx: { p: 1.5, borderRadius: 1.5, bgcolor: 'action.hover', whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }, children: state.error?.message ?? String(state.error) })] }) })] }));
64
151
  }
65
152
  const nodes = nodesOf(state.report);
66
- const elsewhere = nodes.filter((node) => node.placement === 'remote').length;
67
- return (_jsxs(Box, { sx: { p: 3, display: 'grid', gap: 2 }, children: [title, _jsxs(Row, { children: [_jsxs(Typography, { variant: "body2", color: "text.secondary", children: [label('topology.here_count', `${nodes.length - elsewhere} here`, { smart_count: nodes.length - elsewhere }), ' · ', label('topology.elsewhere_count', `${elsewhere} elsewhere`, { smart_count: elsewhere }), ' · ', label('topology.paths', `${state.report.edges.length} observed call paths`, { smart_count: state.report.edges.length })] }), state.report.active > 0 && (_jsx(Chip, { size: "small", color: "primary", label: label('topology.inFlight', `${state.report.active} in flight`, { smart_count: state.report.active }) }))] }), _jsx(Box, { sx: { display: 'grid', gap: 2, gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))' }, children: nodes.map((node) => _jsx(FrondCard, { node: node }, node.frond)) }), state.report.edges.length === 0 && (_jsx(Typography, { variant: "caption", color: "text.secondary", children: label('topology.noEdges', 'No call between fronds observed yet — an edge appears the first time one calls another.') }))] }));
153
+ // Three counts because there are three ways of being known, and `elsewhere` means ANSWERED
154
+ // from elsewhere: counting a frond that never answered among them makes the summary
155
+ // contradict the card under it.
156
+ const unheard = nodes.filter((node) => node.silent).length;
157
+ const elsewhere = nodes.filter((node) => node.placement === 'remote' && !node.silent).length;
158
+ const shown = nodes.find((node) => node.frond === selected) ?? nodes[0];
159
+ const travelled = new Set(state.report.edges.map((edge) => `${edge.from} ${edge.to}`));
160
+ const quiet = state.report.declared.edges.filter((edge) => !travelled.has(`${edge.from} ${edge.to}`)).length;
161
+ return (_jsxs(Box, { sx: { p: 3, display: 'grid', gap: 2 }, children: [title, _jsxs(Row, { children: [_jsxs(Typography, { variant: "body2", color: "text.secondary", children: [label('topology.here_count', `${nodes.length - elsewhere - unheard} here`, { smart_count: nodes.length - elsewhere - unheard }), ' · ', label('topology.elsewhere_count', `${elsewhere} elsewhere`, { smart_count: elsewhere }), unheard > 0 && _jsxs(_Fragment, { children: [' · ', label('topology.unheard_count', `${unheard} unheard`, { smart_count: unheard })] }), ' · ', label('topology.paths', `${state.report.edges.length} observed call paths`, { smart_count: state.report.edges.length }), quiet > 0 && _jsxs(_Fragment, { children: [' · ', label('topology.quiet_count', `${quiet} link without traffic`, { smart_count: quiet })] })] }), state.report.active > 0 && (_jsx(Chip, { size: "small", color: "primary", label: label('topology.inFlight', `${state.report.active} in flight`, { smart_count: state.report.active }) }))] }), _jsx(Graph, { nodes: nodes, selected: shown?.frond, onSelect: setSelected }), shown && _jsx(Box, { sx: { maxWidth: 520 }, children: _jsx(FrondCard, { node: shown }) }), state.report.edges.length === 0 && (_jsx(Typography, { variant: "caption", color: "text.secondary", children: label('topology.noEdges', 'No call between fronds observed yet — an edge appears the first time one calls another.') }))] }));
68
162
  }
69
163
  //# sourceMappingURL=topology-page.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"topology-page.js","sourceRoot":"","sources":["../src/topology-page.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AACnF,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAqB,MAAM,OAAO,CAAC;AAC/D,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,OAAO,IAAI,cAAc,EAAgB,MAAM,qBAAqB,CAAC;AAC7F,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,OAAO,EAAqD,MAAM,eAAe,CAAC;AASpH;;;;GAIG;AACH,SAAS,SAAS;IAChB,MAAM,SAAS,GAAG,YAAY,EAAE,CAAC;IACjC,OAAO,CAAC,GAAW,EAAE,QAAgB,EAAE,OAAiC,EAAE,EAAE,CAC1E,SAAS,CAAC,iBAAiB,GAAG,EAAE,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,GAAG,GAAG,CAAC,EAAE,QAAQ,EAAiC,EAAE,EAAE,CAAC,CAC3D,KAAC,GAAG,IAAC,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAG,QAAQ,GAAO,CAC/F,CAAC;AAEF,uFAAuF;AACvF,SAAS,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAA2C;IAC5E,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC;IAC1B,OAAO,CACL,MAAC,GAAG,eACF,KAAC,UAAU,IAAC,OAAO,EAAC,SAAS,EAAC,EAAE,EAAE,EAAE,UAAU,EAAE,WAAW,EAAE,YAC1D,SAAS,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,IAAI,EAAE,GAC7C,EACb,KAAC,UAAU,IAAC,OAAO,EAAC,SAAS,EAAC,KAAK,EAAC,gBAAgB,YAAE,IAAI,CAAC,KAAK,GAAc,EAC7E,IAAI,CAAC,MAAM,GAAG,CAAC,IAAI,CAClB,KAAC,IAAI,IACH,IAAI,EAAC,OAAO,EACZ,KAAK,EAAC,OAAO,EACb,KAAK,EAAE,KAAK,CAAC,kBAAkB,EAAE,GAAG,IAAI,CAAC,MAAM,UAAU,EAAE,EAAE,WAAW,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,GACxF,CACH,IACG,CACP,CAAC;AACJ,CAAC;AAED,SAAS,SAAS,CAAC,EAAE,IAAI,EAA0B;IACjD,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC;IAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,KAAK,OAAO,CAAC;IACxC,OAAO,CACL,KAAC,IAAI,IAAC,OAAO,EAAC,UAAU,YACtB,MAAC,WAAW,IAAC,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,aAC1C,MAAC,GAAG,eACF,KAAC,UAAU,IAAC,OAAO,EAAC,IAAI,EAAC,EAAE,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE,YAAG,IAAI,CAAC,KAAK,GAAc,EAC3E,KAAC,IAAI,IACH,IAAI,EAAC,OAAO,EACZ,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EACnC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,oBAAoB,EAAE,WAAW,CAAC,GACvF,IACE,EAMN,KAAC,UAAU,IAAC,OAAO,EAAC,OAAO,EAAC,KAAK,EAAC,gBAAgB,YAC/C,QAAQ,CAAC,IAAI,CAAC;wBACb,CAAC,CAAC,KAAK,CAAC,iBAAiB,EAAE,qDAAqD,CAAC;wBACjF,CAAC,CAAC,GAAG,KAAK,CAAC,mBAAmB,EAAE,GAAG,IAAI,CAAC,QAAQ,WAAW,EAAE,EAAE,WAAW,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,EAAE;8BAC1F,MAAM,KAAK,CAAC,gBAAgB,EAAE,GAAG,IAAI,CAAC,KAAK,QAAQ,EAAE,EAAE,WAAW,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,EAAE,GAC9E,EACZ,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAC,QAAQ,IAAwB,IAAI,EAAE,IAAI,EAAE,SAAS,EAAC,KAAK,IAA7C,OAAO,IAAI,CAAC,EAAE,EAAE,CAAgC,CAAC,EACzF,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAC,QAAQ,IAAyB,IAAI,EAAE,IAAI,EAAE,SAAS,EAAC,IAAI,IAA7C,MAAM,IAAI,CAAC,IAAI,EAAE,CAA+B,CAAC,IACjF,GACT,CACR,CAAC;AACJ,CAAC;AAOD,MAAM,UAAU,eAAe,CAAC,EAAE,QAAQ,GAAG,aAAa,EAAE,OAAO,GAAG,cAAc,EAAE,GAAyB,EAAE;IAC/G,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC;IAC1B,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;IAEjE,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,IAAI,GAAG,IAAI,CAAC;QAChB,aAAa,CAAC,QAAQ,EAAE,OAAO,CAAC;aAC7B,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,GAAG,IAAI,IAAI;YAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;aAC3G,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,GAAG,IAAI,IAAI;YAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACnF,OAAO,GAAG,EAAE,GAAG,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IACjC,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;IAExB,MAAM,KAAK,GAAG,KAAC,KAAK,IAAC,KAAK,EAAE,KAAK,CAAC,gBAAgB,EAAE,UAAU,CAAC,GAAI,CAAC;IAEpE,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,MAAC,GAAG,IAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,aAAG,KAAK,EAAC,KAAC,QAAQ,IAAC,MAAM,EAAE,GAAG,GAAI,EAAA,KAAC,QAAQ,IAAC,MAAM,EAAE,GAAG,GAAI,IAAM,CAAC;IAC5F,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,YAAY,EAAE,CAAC;QAClC,OAAO,CACL,MAAC,GAAG,IAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,aAC7B,KAAK,EACN,KAAC,IAAI,cACH,MAAC,WAAW,IAAC,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,aAC5C,KAAC,UAAU,IAAC,OAAO,EAAC,IAAI,YAAE,KAAK,CAAC,0BAA0B,EAAE,kCAAkC,CAAC,GAAc,EAC7G,KAAC,UAAU,IAAC,OAAO,EAAC,OAAO,EAAC,KAAK,EAAC,gBAAgB,YAC/C,KAAK,CACJ,yBAAyB,EACzB,6KAA6K,CAC9K,GACU,EACb,KAAC,UAAU,IAAC,SAAS,EAAC,KAAK,EAAC,OAAO,EAAC,SAAS,EAAC,EAAE,EAAE,EAAE,CAAC,EAAE,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAE,YACxH,0FAA0F,GAChF,IACD,GACT,IACH,CACP,CAAC;IACJ,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,CACL,MAAC,GAAG,IAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,aAC7B,KAAK,EACN,KAAC,IAAI,cACH,MAAC,WAAW,IAAC,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,aAC5C,KAAC,UAAU,IAAC,OAAO,EAAC,IAAI,YAAE,KAAK,CAAC,sBAAsB,EAAE,wBAAwB,CAAC,GAAc,EAC/F,KAAC,UAAU,IAAC,SAAS,EAAC,KAAK,EAAC,OAAO,EAAC,SAAS,EAAC,EAAE,EAAE,EAAE,CAAC,EAAE,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,YACtJ,KAAK,CAAC,KAAe,EAAE,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,GAC5C,IACD,GACT,IACH,CACP,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACpC,MAAM,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,MAAM,CAAC;IAE7E,OAAO,CACL,MAAC,GAAG,IAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,aACvC,KAAK,EACN,MAAC,GAAG,eACF,MAAC,UAAU,IAAC,OAAO,EAAC,OAAO,EAAC,KAAK,EAAC,gBAAgB,aAC/C,KAAK,CAAC,qBAAqB,EAAE,GAAG,KAAK,CAAC,MAAM,GAAG,SAAS,OAAO,EAAE,EAAE,WAAW,EAAE,KAAK,CAAC,MAAM,GAAG,SAAS,EAAE,CAAC,EAC3G,KAAK,EACL,KAAK,CAAC,0BAA0B,EAAE,GAAG,SAAS,YAAY,EAAE,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC,EACvF,KAAK,EACL,KAAK,CAAC,gBAAgB,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,sBAAsB,EAAE,EAAE,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,IAC7G,EAEZ,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,CAC1B,KAAC,IAAI,IACH,IAAI,EAAC,OAAO,EACZ,KAAK,EAAC,SAAS,EACf,KAAK,EAAE,KAAK,CAAC,mBAAmB,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,YAAY,EAAE,EAAE,WAAW,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAC3G,CACH,IACG,EACN,KAAC,GAAG,IAAC,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,mBAAmB,EAAE,uCAAuC,EAAE,YAC/F,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAC,SAAS,IAAkB,IAAI,EAAE,IAAI,IAAtB,IAAI,CAAC,KAAK,CAAgB,CAAC,GAC5D,EAKL,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,CAClC,KAAC,UAAU,IAAC,OAAO,EAAC,SAAS,EAAC,KAAK,EAAC,gBAAgB,YACjD,KAAK,CAAC,kBAAkB,EAAE,yFAAyF,CAAC,GAC1G,CACd,IACG,CACP,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"topology-page.js","sourceRoot":"","sources":["../src/topology-page.tsx"],"names":[],"mappings":"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"}
@@ -1,7 +1,6 @@
1
- /** How the system is arranged, as the app itself reports it. */
2
1
  import { type Fetcher } from '@fougere/app/client';
3
- import { type Edge, type FrondPlacement, type TopologyReport } from '@fougere/core/contract';
4
- export type { TopologyReport, FrondPlacement, Edge };
2
+ import { type DeclaredEdge, type Edge, type FrondPlacement, type TopologyReport } from '@fougere/core/contract';
3
+ export type { TopologyReport, FrondPlacement, Edge, DeclaredEdge };
5
4
  /** The report, or `undefined` when the app serves no topology at all. */
6
5
  export declare function fetchTopology(endpoint?: string, fetcher?: Fetcher): Promise<TopologyReport | undefined>;
7
6
  /** One frond as the page draws it: its placement, and the calls observed around it. */
@@ -10,9 +9,72 @@ export interface TopologyNode extends FrondPlacement {
10
9
  calls: Edge[];
11
10
  /** Fronds that called this one. */
12
11
  calledBy: Edge[];
12
+ /** Fronds its code reaches, whether or not a call has ever gone down the link. */
13
+ declaredCalls: DeclaredEdge[];
14
+ /** Where the config says to reach it, when it says so. */
15
+ at?: string;
16
+ /**
17
+ * Declared in `remotes:` and never heard from. A different absence from an opaque remote,
18
+ * which answered and keeps its shape to itself: this one may simply be down.
19
+ */
20
+ silent: boolean;
13
21
  }
14
- /** The report read as a graph, local fronds first. */
22
+ /** The report read as a graph — what runs here first, then what answered, then what never did. */
15
23
  export declare function nodesOf(report: TopologyReport): TopologyNode[];
16
- /** A frond this process called that publishes nothing of its own shape. */
24
+ export interface Point {
25
+ x: number;
26
+ y: number;
27
+ }
28
+ /** A node placed on the drawing. `x`/`y` are its CENTRE, the convention the layout answers in. */
29
+ export interface PlacedNode {
30
+ node: TopologyNode;
31
+ x: number;
32
+ y: number;
33
+ width: number;
34
+ height: number;
35
+ }
36
+ /**
37
+ * One link between two nodes. `count` is absent on a link nothing has travelled: the config
38
+ * declares it and no call has gone down it, which is drawn broken rather than not drawn.
39
+ */
40
+ export interface PlacedEdge {
41
+ from: string;
42
+ to: string;
43
+ path: string;
44
+ /** Where this link's figure goes — clear of every node, or absent when the route is a point. */
45
+ at?: Point;
46
+ count?: number;
47
+ errors: number;
48
+ /** Where this link's volume sits among the others, `0` to `1`. Absent when nothing travelled. */
49
+ weight?: number;
50
+ }
51
+ export interface Drawing {
52
+ nodes: PlacedNode[];
53
+ edges: PlacedEdge[];
54
+ width: number;
55
+ height: number;
56
+ }
57
+ /**
58
+ * Where each frond sits, and the line between two of them.
59
+ *
60
+ * The placement is `@dagrejs/dagre`'s — ranks by depth, order within a rank chosen to cross as
61
+ * little as possible, and a polyline per edge that goes around the nodes between its ends. Two
62
+ * hand-written columns could not do the second or the third: a chain of three fronds put two of
63
+ * them in one column, and the curve between them had to bulge around whatever sat in the way.
64
+ *
65
+ * Rendering stays ours, which is why only the layout is bought: the drawing reads `error.main`
66
+ * and the rest of the MUI palette off the theme, and a library that painted its own canvas
67
+ * would have to be told the theme again on every switch. Kiali left Cytoscape for this in 2.0.0,
68
+ * and every service map whose source can be read — Grafana, Jaeger, Linkerd — buys the same half.
69
+ */
70
+ export declare function layoutOf(nodes: readonly TopologyNode[]): Drawing;
71
+ /** A count as an operator reads it at a glance — `2.1k`, never `2140`. */
72
+ export declare function short(count: number): string;
73
+ /** What a link says of itself, and the only figure the drawing carries. */
74
+ export declare function figureOf(link: Pick<PlacedEdge, 'count' | 'errors'>): string;
75
+ /**
76
+ * A frond this process called that publishes nothing of its own shape. Read AFTER `silent`:
77
+ * a frond that never answered has the same two zeroes and a different reason for them.
78
+ */
17
79
  export declare function isOpaque(node: FrondPlacement): boolean;
18
80
  //# sourceMappingURL=topology.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"topology.d.ts","sourceRoot":"","sources":["../src/topology.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,OAAO,EAIL,KAAK,OAAO,EACb,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAEL,KAAK,IAAI,EACT,KAAK,cAAc,EACnB,KAAK,cAAc,EAEpB,MAAM,wBAAwB,CAAC;AAEhC,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC;AAErD,yEAAyE;AACzE,wBAAsB,aAAa,CACjC,QAAQ,SAAgB,EACxB,OAAO,GAAE,OAAwB,GAChC,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAYrC;AAED,uFAAuF;AACvF,MAAM,WAAW,YAAa,SAAQ,cAAc;IAClD,sDAAsD;IACtD,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,mCAAmC;IACnC,QAAQ,EAAE,IAAI,EAAE,CAAC;CAClB;AAED,sDAAsD;AACtD,wBAAgB,OAAO,CAAC,MAAM,EAAE,cAAc,GAAG,YAAY,EAAE,CAS9D;AAED,2EAA2E;AAC3E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAEtD"}
1
+ {"version":3,"file":"topology.d.ts","sourceRoot":"","sources":["../src/topology.ts"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,OAAO,EACb,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,IAAI,EACT,KAAK,cAAc,EACnB,KAAK,cAAc,EAEpB,MAAM,wBAAwB,CAAC;AAEhC,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;AAEnE,yEAAyE;AACzE,wBAAsB,aAAa,CACjC,QAAQ,SAAgB,EACxB,OAAO,GAAE,OAAwB,GAChC,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAYrC;AAED,uFAAuF;AACvF,MAAM,WAAW,YAAa,SAAQ,cAAc;IAClD,sDAAsD;IACtD,KAAK,EAAE,IAAI,EAAE,CAAC;IACd,mCAAmC;IACnC,QAAQ,EAAE,IAAI,EAAE,CAAC;IACjB,kFAAkF;IAClF,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,0DAA0D;IAC1D,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,kGAAkG;AAClG,wBAAgB,OAAO,CAAC,MAAM,EAAE,cAAc,GAAG,YAAY,EAAE,CAqC9D;AAED,MAAM,WAAW,KAAK;IACpB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,kGAAkG;AAClG,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,YAAY,CAAC;IACnB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,gGAAgG;IAChG,EAAE,CAAC,EAAE,KAAK,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,iGAAiG;IACjG,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAID;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,GAAG,OAAO,CA6ChE;AAED,0EAA0E;AAC1E,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAI3C;AAED,2EAA2E;AAC3E,wBAAgB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,OAAO,GAAG,QAAQ,CAAC,GAAG,MAAM,CAI3E;AAsGD;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAEtD"}
package/dist/topology.js CHANGED
@@ -1,4 +1,5 @@
1
1
  /** How the system is arranged, as the app itself reports it. */
2
+ import dagre from '@dagrejs/dagre';
2
3
  import { CALL_ENDPOINT, fetcher as browserFetcher, sendCall, } from '@fougere/app/client';
3
4
  import { ErrorCode, Invocation, } from '@fougere/core/contract';
4
5
  /** The report, or `undefined` when the app serves no topology at all. */
@@ -12,18 +13,200 @@ export async function fetchTopology(endpoint = CALL_ENDPOINT, fetcher = browserF
12
13
  throw error;
13
14
  }
14
15
  }
15
- /** The report read as a graph, local fronds first. */
16
+ /** The report read as a graph — what runs here first, then what answered, then what never did. */
16
17
  export function nodesOf(report) {
17
18
  const order = { local: 0, remote: 1 };
18
- return [...report.fronds]
19
+ const stated = new Map(report.declared.fronds.map((one) => [one.frond, one]));
20
+ const answered = new Set(report.fronds.map((frond) => frond.frond));
21
+ const around = (frond) => ({
22
+ calls: report.edges.filter((edge) => edge.from === frond),
23
+ calledBy: report.edges.filter((edge) => edge.to === frond),
24
+ declaredCalls: report.declared.edges.filter((edge) => edge.from === frond),
25
+ });
26
+ const heard = [...report.fronds]
19
27
  .sort((a, b) => order[a.placement] - order[b.placement] || a.frond.localeCompare(b.frond))
20
28
  .map((frond) => ({
21
29
  ...frond,
22
- calls: report.edges.filter((edge) => edge.from === frond.frond),
23
- calledBy: report.edges.filter((edge) => edge.to === frond.frond),
30
+ ...around(frond.frond),
31
+ ...(stated.get(frond.frond)?.at ? { at: stated.get(frond.frond).at } : {}),
32
+ silent: false,
24
33
  }));
34
+ // The whole reason the declared half travels: a frond that never answered is absent from
35
+ // the observed one, so without this the node the operator most needs to see is the one
36
+ // that disappears.
37
+ const silent = report.declared.fronds
38
+ .filter((one) => one.placement === 'remote' && !answered.has(one.frond))
39
+ .sort((a, b) => a.frond.localeCompare(b.frond))
40
+ .map((one) => ({
41
+ frond: one.frond,
42
+ placement: 'remote',
43
+ entities: 0,
44
+ doors: 0,
45
+ ...around(one.frond),
46
+ ...(one.at ? { at: one.at } : {}),
47
+ silent: true,
48
+ }));
49
+ return [...heard, ...silent];
50
+ }
51
+ const NODE = { width: 176, height: 54 };
52
+ /**
53
+ * Where each frond sits, and the line between two of them.
54
+ *
55
+ * The placement is `@dagrejs/dagre`'s — ranks by depth, order within a rank chosen to cross as
56
+ * little as possible, and a polyline per edge that goes around the nodes between its ends. Two
57
+ * hand-written columns could not do the second or the third: a chain of three fronds put two of
58
+ * them in one column, and the curve between them had to bulge around whatever sat in the way.
59
+ *
60
+ * Rendering stays ours, which is why only the layout is bought: the drawing reads `error.main`
61
+ * and the rest of the MUI palette off the theme, and a library that painted its own canvas
62
+ * would have to be told the theme again on every switch. Kiali left Cytoscape for this in 2.0.0,
63
+ * and every service map whose source can be read — Grafana, Jaeger, Linkerd — buys the same half.
64
+ */
65
+ export function layoutOf(nodes) {
66
+ const graph = new dagre.graphlib.Graph();
67
+ // Left to right: the reader is the process being asked, and what it depends on goes outward.
68
+ graph.setGraph({ rankdir: 'LR', nodesep: 26, ranksep: 92, marginx: 22, marginy: 22 });
69
+ graph.setDefaultEdgeLabel(() => ({}));
70
+ for (const node of nodes)
71
+ graph.setNode(node.frond, { ...NODE });
72
+ const links = linksBetween(nodes);
73
+ for (const link of links) {
74
+ if (graph.hasNode(link.from) && graph.hasNode(link.to))
75
+ graph.setEdge(link.from, link.to);
76
+ }
77
+ dagre.layout(graph);
78
+ const travelled = links.map((link) => link.count).filter((count) => count !== undefined);
79
+ const shape = graph.graph();
80
+ const placed = nodes
81
+ .filter((node) => graph.hasNode(node.frond))
82
+ .map((node) => {
83
+ const { x, y, width, height } = graph.node(node.frond);
84
+ return { node, x, y, width, height };
85
+ });
86
+ return {
87
+ nodes: placed,
88
+ edges: links.flatMap((link) => {
89
+ const drawn = graph.hasNode(link.from) && graph.hasNode(link.to) ? graph.edge(link.from, link.to) : undefined;
90
+ if (!drawn)
91
+ return [];
92
+ const points = drawn.points;
93
+ const at = labelAt(points, placed, figureOf(link).length * 3.4 + 8);
94
+ return [{
95
+ ...link,
96
+ path: through(points),
97
+ ...(at ? { at } : {}),
98
+ ...(link.count === undefined ? {} : { weight: weightOf(link.count, travelled) }),
99
+ }];
100
+ }),
101
+ width: shape.width ?? 0,
102
+ height: shape.height ?? 0,
103
+ };
104
+ }
105
+ /** A count as an operator reads it at a glance — `2.1k`, never `2140`. */
106
+ export function short(count) {
107
+ if (count < 1000)
108
+ return String(count);
109
+ return `${(count / 1000).toFixed(count < 10_000 ? 1 : 0)}k`;
110
+ }
111
+ /** What a link says of itself, and the only figure the drawing carries. */
112
+ export function figureOf(link) {
113
+ if (link.count === undefined)
114
+ return '—';
115
+ return link.errors > 0 ? `${short(link.count)} · ${short(link.errors)} ✕` : short(link.count);
116
+ }
117
+ /**
118
+ * Where a volume sits among the others, on a SQUARE ROOT scale. Linear makes every link but the
119
+ * busiest a hairline; a log flattens them the other way, and flattening is wrong at this spread —
120
+ * 40 to 2140 is under two orders of magnitude, and the busiest link deserves to look it.
121
+ */
122
+ function weightOf(count, travelled) {
123
+ const high = Math.max(...travelled);
124
+ if (high <= 0)
125
+ return 1;
126
+ return Math.sqrt(count / high);
127
+ }
128
+ /** Every link, observed or merely declared, each named once. */
129
+ function linksBetween(nodes) {
130
+ const observed = nodes.flatMap((one) => one.calls);
131
+ const travelled = new Set(observed.map((edge) => `${edge.from} ${edge.to}`));
132
+ return [
133
+ ...observed.map((edge) => ({ from: edge.from, to: edge.to, count: edge.count, errors: edge.errors })),
134
+ ...nodes
135
+ .flatMap((one) => one.declaredCalls)
136
+ .filter((edge) => !travelled.has(`${edge.from} ${edge.to}`))
137
+ .map((edge) => ({ from: edge.from, to: edge.to, errors: 0 })),
138
+ ];
139
+ }
140
+ /**
141
+ * A B-spline over the waypoints — what dagre-d3 and Grafana draw their edges with.
142
+ *
143
+ * It deliberately does NOT pass through them. A curve pinned to the points takes its character
144
+ * from their COUNT, and a router hands back two for a short hop and five for one that goes around
145
+ * something — so every edge came out with a bearing of its own and the drawing read as noise.
146
+ *
147
+ * The ends are TRIPLED rather than joined by a straight segment: a clamped spline reaches its
148
+ * endpoint on its own tangent, where an anchor line meets the curve at whatever angle it happens
149
+ * to, which is a visible corner exactly where the arrow is.
150
+ */
151
+ function through(points) {
152
+ const first = points[0];
153
+ const last = points.at(-1);
154
+ if (!first || !last)
155
+ return '';
156
+ if (points.length === 1)
157
+ return `M ${first.x} ${first.y}`;
158
+ const held = [first, first, ...points, last, last];
159
+ const steps = held.slice(2).map((point, nth) => {
160
+ const back = held[nth];
161
+ const near = held[nth + 1];
162
+ return `C ${(2 * back.x + near.x) / 3} ${(2 * back.y + near.y) / 3},`
163
+ + ` ${(back.x + 2 * near.x) / 3} ${(back.y + 2 * near.y) / 3},`
164
+ + ` ${(back.x + 4 * near.x + point.x) / 6} ${(back.y + 4 * near.y + point.y) / 6}`;
165
+ });
166
+ return `M ${first.x} ${first.y} ${steps.join(' ')}`;
167
+ }
168
+ /** A point at a fraction of a route's length. */
169
+ function alongOf(points, fraction) {
170
+ if (fraction <= 0 || fraction >= 1)
171
+ return undefined;
172
+ const spans = points.slice(1).map((point, nth) => Math.hypot(point.x - points[nth].x, point.y - points[nth].y));
173
+ const target = spans.reduce((sum, span) => sum + span, 0) * fraction;
174
+ let walked = 0;
175
+ for (let nth = 0; nth < spans.length; nth++) {
176
+ if (walked + spans[nth] >= target) {
177
+ const into = spans[nth] === 0 ? 0 : (target - walked) / spans[nth];
178
+ return {
179
+ x: points[nth].x + (points[nth + 1].x - points[nth].x) * into,
180
+ y: points[nth].y + (points[nth + 1].y - points[nth].y) * into,
181
+ };
182
+ }
183
+ walked += spans[nth];
184
+ }
185
+ return points.at(-1);
186
+ }
187
+ /**
188
+ * Where a link's figure sits: halfway ALONG the route, then moved along it until it sits inside no
189
+ * node. The middle waypoint is not the middle of the line — a routed edge bunches its points near
190
+ * the ends — and a figure drawn over a card cannot be read at all.
191
+ */
192
+ function labelAt(points, boxes, half) {
193
+ const inside = (point) => boxes.some((box) => Math.abs(point.x - box.x) < box.width / 2 + half && Math.abs(point.y - box.y) < box.height / 2 + 13);
194
+ const middle = alongOf(points, 0.5);
195
+ if (!middle || !inside(middle))
196
+ return middle;
197
+ for (let step = 1; step <= 12; step++) {
198
+ for (const side of [-1, 1]) {
199
+ const moved = alongOf(points, 0.5 + side * step * 0.035);
200
+ if (moved && !inside(moved))
201
+ return moved;
202
+ }
203
+ }
204
+ return middle;
25
205
  }
26
- /** A frond this process called that publishes nothing of its own shape. */
206
+ /**
207
+ * A frond this process called that publishes nothing of its own shape. Read AFTER `silent`:
208
+ * a frond that never answered has the same two zeroes and a different reason for them.
209
+ */
27
210
  export function isOpaque(node) {
28
211
  return node.placement === 'remote' && node.entities === 0 && node.doors === 0;
29
212
  }
@@ -1 +1 @@
1
- {"version":3,"file":"topology.js","sourceRoot":"","sources":["../src/topology.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,OAAO,EACL,aAAa,EACb,OAAO,IAAI,cAAc,EACzB,QAAQ,GAET,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,SAAS,EAIT,UAAU,GACX,MAAM,wBAAwB,CAAC;AAIhC,yEAAyE;AACzE,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,QAAQ,GAAG,aAAa,EACxB,OAAO,GAAY,cAAc;IAEjC,IAAI,CAAC;QACH,OAAO,MAAM,QAAQ,CACnB,OAAO,EACP,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE,UAAU,EAAE,EACjC,UAAU,CAAC,KAAK,EAChB,QAAQ,CACS,CAAC;IACtB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAK,KAA4B,EAAE,IAAI,KAAK,SAAS,CAAC,SAAS;YAAE,OAAO,SAAS,CAAC;QAClF,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAUD,sDAAsD;AACtD,MAAM,UAAU,OAAO,CAAC,MAAsB;IAC5C,MAAM,KAAK,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IACtC,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;SACtB,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;SACzF,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACf,GAAG,KAAK;QACR,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,KAAK,CAAC,KAAK,CAAC;QAC/D,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,KAAK,CAAC,KAAK,CAAC;KACjE,CAAC,CAAC,CAAC;AACR,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,QAAQ,CAAC,IAAoB;IAC3C,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,IAAI,IAAI,CAAC,QAAQ,KAAK,CAAC,IAAI,IAAI,CAAC,KAAK,KAAK,CAAC,CAAC;AAChF,CAAC"}
1
+ {"version":3,"file":"topology.js","sourceRoot":"","sources":["../src/topology.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fougere/admin",
3
- "version": "0.9.2-alpha.0",
3
+ "version": "0.9.3-alpha.0",
4
4
  "description": "A back-office derived from what the app already says it hosts — a react-admin DataProvider over the Fougere wire.",
5
5
  "keywords": [
6
6
  "fougere",
@@ -34,9 +34,10 @@
34
34
  "src"
35
35
  ],
36
36
  "dependencies": {
37
- "@fougere/app": "0.9.2-alpha.0",
38
- "@fougere/core": "0.9.2-alpha.0",
39
- "@fougere/schema": "0.9.2-alpha.0"
37
+ "@dagrejs/dagre": "^3.1.1",
38
+ "@fougere/app": "0.9.3-alpha.0",
39
+ "@fougere/core": "0.9.3-alpha.0",
40
+ "@fougere/schema": "0.9.3-alpha.0"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@mui/material": "^9.3.1",
@@ -5,18 +5,20 @@
5
5
  *
6
6
  * Every other page in this panel renders a DOOR — a list, a form, a row. This one renders
7
7
  * the app: which fronds run in the process it is talking to, which answered from somewhere
8
- * else, and who called whom. Nothing here is configured, and nothing is declared: a frond
9
- * is remote because it answered a call nobody hosts.
8
+ * else, who called whom — and which the config names but nothing has ever answered from.
9
+ * The first three are OBSERVED: a frond is elsewhere because it answered, never because
10
+ * `remotes:` said so. The fourth is the only declared one, and it is the only one the
11
+ * observed half cannot hold — a frond that never answered is absent from it.
10
12
  *
11
13
  * It is served by `@fougere/observability`, so its absence is a legible state and not an
12
14
  * error — which is why the entry stays in the menu when nothing answers. Hiding it would
13
15
  * hide the one place that can say what is missing.
14
16
  */
15
- import { Box, Card, CardContent, Chip, Skeleton, Typography } from '@mui/material';
16
- import { useEffect, useState, type ReactElement } from 'react';
17
+ import { Box, Card, CardContent, Chip, Skeleton, Typography, type Theme } from '@mui/material';
18
+ import { useEffect, useMemo, useState, type ReactElement } from 'react';
17
19
  import { Title, useTranslate } from 'react-admin';
18
20
  import { CALL_ENDPOINT, fetcher as browserFetcher, type Fetcher } from '@fougere/app/client';
19
- import { fetchTopology, isOpaque, nodesOf, type Edge, type TopologyNode, type TopologyReport } from './topology.js';
21
+ import { fetchTopology, figureOf, isOpaque, layoutOf, nodesOf, type Edge, type TopologyNode, type TopologyReport } from './topology.js';
20
22
 
21
23
  /** What the page is: the report, the refusal that means "not observed", or a real failure. */
22
24
  type State =
@@ -60,6 +62,167 @@ function EdgeLine({ edge, direction }: { edge: Edge; direction: 'out' | 'in' }):
60
62
  );
61
63
  }
62
64
 
65
+ /**
66
+ * A palette path as SVG paint. `sx` resolves `'error.main'` for `color` and `bgcolor` and for
67
+ * nothing else, so `fill` and `stroke` reach CSS verbatim — an invalid value, and SVG falls back
68
+ * to BLACK. Every shape on this page is painted through here.
69
+ */
70
+ function paint(paths: { fill?: string; stroke?: string }) {
71
+ const read = (theme: Theme, path: string) =>
72
+ path.split('.').reduce<unknown>((held, key) => (held as Record<string, unknown>)?.[key], theme.palette) as string;
73
+
74
+ return (theme: Theme) => ({
75
+ ...(paths.fill ? { fill: read(theme, paths.fill) } : {}),
76
+ ...(paths.stroke ? { stroke: read(theme, paths.stroke) } : {}),
77
+ });
78
+ }
79
+
80
+ /** What a node is painted with, by the one thing that separates the three states. */
81
+ function toneOf(node: TopologyNode): 'primary' | 'grey' | 'warning' {
82
+ if (node.silent) return 'warning';
83
+
84
+ return node.placement === 'local' ? 'primary' : 'grey';
85
+ }
86
+
87
+ /**
88
+ * The system, drawn. Placement is `layoutOf`'s — ranks, order and routing.
89
+ *
90
+ * The nodes are HTML over an SVG that draws only the lines, which is how React Flow is built and
91
+ * why its nodes look like the rest of an app: a card is a `Box` with `sx`, so it reads the theme
92
+ * like every other element. An SVG `<text>` positioned by hand does not.
93
+ *
94
+ * Both layers sit on ONE scale — the container takes the drawing's aspect ratio, and every node is
95
+ * sized AND placed in per cent of it. A size in pixels does not grow with the viewBox the SVG
96
+ * scales to its container, and the lines then stop short of the cards they point at.
97
+ *
98
+ * What the drawing says beyond who reaches whom: thickness is volume, a broken line is declared
99
+ * and never travelled, and refusals are a figure rather than a red line — colour alone excludes a
100
+ * reader who cannot see it, and "how many" is the question a red line raises without answering.
101
+ */
102
+ function Graph({ nodes, selected, onSelect }: {
103
+ nodes: readonly TopologyNode[];
104
+ selected: string | undefined;
105
+ onSelect: (frond: string) => void;
106
+ }): ReactElement {
107
+ const label = useLabels();
108
+ const drawing = useMemo(() => layoutOf(nodes), [nodes]);
109
+ const per = (value: number, of: number) => `${(value / of) * 100}%`;
110
+
111
+ return (
112
+ <Card variant="outlined">
113
+ <Box sx={{ position: 'relative', bgcolor: 'action.hover', aspectRatio: `${drawing.width} / ${drawing.height}` }}>
114
+ <Box
115
+ component="svg"
116
+ viewBox={`0 0 ${drawing.width} ${drawing.height}`}
117
+ sx={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}
118
+ >
119
+ <defs>
120
+ <marker id="fougere-arrow" viewBox="0 0 8 8" refX="6.5" refY="4" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
121
+ <Box component="path" d="M 0 0 L 8 4 L 0 8 z" sx={paint({ fill: 'text.disabled' })} />
122
+ </marker>
123
+ </defs>
124
+
125
+ {drawing.edges.map((edge) => (
126
+ <Box
127
+ component="path"
128
+ key={`${edge.from}-${edge.to}`}
129
+ d={edge.path}
130
+ fill="none"
131
+ strokeLinecap="round"
132
+ strokeWidth={edge.weight === undefined ? 1.5 : 1.5 + edge.weight * 3}
133
+ strokeDasharray={edge.weight === undefined ? '2 5' : undefined}
134
+ markerEnd="url(#fougere-arrow)"
135
+ sx={[paint({ stroke: 'text.disabled' }), { strokeOpacity: 0.5 }]}
136
+ />
137
+ ))}
138
+ </Box>
139
+
140
+ {drawing.edges.map((edge) => edge.at && (
141
+ <Box
142
+ key={`figure-${edge.from}-${edge.to}`}
143
+ sx={{
144
+ position: 'absolute',
145
+ left: per(edge.at.x, drawing.width),
146
+ top: per(edge.at.y, drawing.height),
147
+ transform: 'translate(-50%, -50%)',
148
+ px: 0.75,
149
+ borderRadius: 1,
150
+ border: 1,
151
+ borderColor: 'divider',
152
+ bgcolor: 'background.paper',
153
+ fontFamily: 'monospace',
154
+ fontSize: 11,
155
+ fontWeight: 600,
156
+ lineHeight: '17px',
157
+ whiteSpace: 'nowrap',
158
+ color: edge.errors > 0 ? 'error.main' : 'text.secondary',
159
+ }}
160
+ >
161
+ {edge.count === undefined ? label('topology.noTraffic', 'no traffic') : figureOf(edge)}
162
+ </Box>
163
+ ))}
164
+
165
+ {drawing.nodes.map(({ node, x, y, width, height }) => {
166
+ const tone = toneOf(node);
167
+ return (
168
+ <Box
169
+ key={node.frond}
170
+ onClick={() => onSelect(node.frond)}
171
+ sx={{
172
+ position: 'absolute',
173
+ left: per(x, drawing.width),
174
+ top: per(y, drawing.height),
175
+ width: per(width, drawing.width),
176
+ height: per(height, drawing.height),
177
+ transform: 'translate(-50%, -50%)',
178
+ display: 'flex',
179
+ flexDirection: 'column',
180
+ justifyContent: 'center',
181
+ gap: 0.25,
182
+ overflow: 'hidden',
183
+ px: 1.25,
184
+ borderRadius: 1.5,
185
+ border: node.frond === selected ? 2 : 1.4,
186
+ borderStyle: node.silent ? 'dashed' : 'solid',
187
+ borderColor: tone === 'grey' ? 'divider' : `${tone}.main`,
188
+ bgcolor: 'background.paper',
189
+ cursor: 'pointer',
190
+ }}
191
+ >
192
+ <Box sx={{ fontFamily: 'monospace', fontSize: 13.5, fontWeight: 600 }}>{node.frond}</Box>
193
+ <Row>
194
+ {/* A remote's two zeroes are not a measurement: its shape is published by the
195
+ process that owns it, so the card says WHERE it is instead of counting. */}
196
+ {!node.silent && (
197
+ <Typography variant="caption" color="text.secondary" sx={{ fontSize: 10.5 }}>
198
+ {isOpaque(node)
199
+ ? label('topology.elsewhere', 'elsewhere')
200
+ : `${node.entities} · ${node.doors}`}
201
+ </Typography>
202
+ )}
203
+ {tone !== 'grey' && (
204
+ <Chip
205
+ size="small"
206
+ color={tone}
207
+ variant="outlined"
208
+ sx={{ height: 16, '& .MuiChip-label': { px: 0.75, fontSize: 9.5, letterSpacing: '0.05em' } }}
209
+ label={node.silent ? label('topology.unheard', 'unheard') : label('topology.here', 'here')}
210
+ />
211
+ )}
212
+ </Row>
213
+ </Box>
214
+ );
215
+ })}
216
+ </Box>
217
+ <Box sx={{ px: 2, py: 1.25 }}>
218
+ <Typography variant="caption" color="text.secondary">
219
+ {label('topology.legend', 'Thickness is volume. A broken line is declared and never travelled. Click a frond for what it holds.')}
220
+ </Typography>
221
+ </Box>
222
+ </Card>
223
+ );
224
+ }
225
+
63
226
  function FrondCard({ node }: { node: TopologyNode }): ReactElement {
64
227
  const label = useLabels();
65
228
  const here = node.placement === 'local';
@@ -70,8 +233,10 @@ function FrondCard({ node }: { node: TopologyNode }): ReactElement {
70
233
  <Typography variant="h6" sx={{ fontWeight: 700 }}>{node.frond}</Typography>
71
234
  <Chip
72
235
  size="small"
73
- color={here ? 'primary' : 'default'}
74
- label={here ? label('topology.here', 'here') : label('topology.elsewhere', 'elsewhere')}
236
+ color={node.silent ? 'warning' : here ? 'primary' : 'default'}
237
+ label={node.silent
238
+ ? label('topology.unheard', 'declared, unheard')
239
+ : here ? label('topology.here', 'here') : label('topology.elsewhere', 'elsewhere')}
75
240
  />
76
241
  </Row>
77
242
  {/*
@@ -80,10 +245,12 @@ function FrondCard({ node }: { node: TopologyNode }): ReactElement {
80
245
  empty frond, which reads as a frond with nothing in it.
81
246
  */}
82
247
  <Typography variant="body2" color="text.secondary">
83
- {isOpaque(node)
84
- ? label('topology.opaque', 'Its shape is published by the process that owns it.')
85
- : `${label('topology.entities', `${node.entities} entities`, { smart_count: node.entities })}`
86
- + ` · ${label('topology.doors', `${node.doors} doors`, { smart_count: node.doors })}`}
248
+ {node.silent
249
+ ? label('topology.silent', `Declared at ${node.at ?? 'no address'} — nothing has answered from it.`, { at: node.at ?? '' })
250
+ : isOpaque(node)
251
+ ? label('topology.opaque', 'Its shape is published by the process that owns it.')
252
+ : `${label('topology.entities', `${node.entities} entities`, { smart_count: node.entities })}`
253
+ + ` · ${label('topology.doors', `${node.doors} doors`, { smart_count: node.doors })}`}
87
254
  </Typography>
88
255
  {node.calls.map((edge) => <EdgeLine key={`out-${edge.to}`} edge={edge} direction="out" />)}
89
256
  {node.calledBy.map((edge) => <EdgeLine key={`in-${edge.from}`} edge={edge} direction="in" />)}
@@ -100,6 +267,8 @@ export interface FougereTopologyProps {
100
267
  export function FougereTopology({ endpoint = CALL_ENDPOINT, fetcher = browserFetcher }: FougereTopologyProps = {}): ReactElement {
101
268
  const label = useLabels();
102
269
  const [state, setState] = useState<State>({ status: 'loading' });
270
+ /** Which frond the detail below is about — the drawing answers "where", one card answers "what". */
271
+ const [selected, setSelected] = useState<string>();
103
272
 
104
273
  useEffect(() => {
105
274
  let live = true;
@@ -154,18 +323,34 @@ export function FougereTopology({ endpoint = CALL_ENDPOINT, fetcher = browserFet
154
323
  }
155
324
 
156
325
  const nodes = nodesOf(state.report);
157
- const elsewhere = nodes.filter((node) => node.placement === 'remote').length;
326
+ // Three counts because there are three ways of being known, and `elsewhere` means ANSWERED
327
+ // from elsewhere: counting a frond that never answered among them makes the summary
328
+ // contradict the card under it.
329
+ const unheard = nodes.filter((node) => node.silent).length;
330
+ const elsewhere = nodes.filter((node) => node.placement === 'remote' && !node.silent).length;
331
+ const shown = nodes.find((node) => node.frond === selected) ?? nodes[0];
332
+ const travelled = new Set(state.report.edges.map((edge) => `${edge.from} ${edge.to}`));
333
+ const quiet = state.report.declared.edges.filter((edge) => !travelled.has(`${edge.from} ${edge.to}`)).length;
158
334
 
159
335
  return (
160
336
  <Box sx={{ p: 3, display: 'grid', gap: 2 }}>
161
337
  {title}
162
338
  <Row>
163
339
  <Typography variant="body2" color="text.secondary">
164
- {label('topology.here_count', `${nodes.length - elsewhere} here`, { smart_count: nodes.length - elsewhere })}
340
+ {label('topology.here_count', `${nodes.length - elsewhere - unheard} here`, { smart_count: nodes.length - elsewhere - unheard })}
165
341
  {' · '}
166
342
  {label('topology.elsewhere_count', `${elsewhere} elsewhere`, { smart_count: elsewhere })}
343
+ {unheard > 0 && <>
344
+ {' · '}
345
+ {label('topology.unheard_count', `${unheard} unheard`, { smart_count: unheard })}
346
+ </>}
167
347
  {' · '}
168
348
  {label('topology.paths', `${state.report.edges.length} observed call paths`, { smart_count: state.report.edges.length })}
349
+ {/* The two anomalies, counted where a reader looks first — not left to be found in the drawing. */}
350
+ {quiet > 0 && <>
351
+ {' · '}
352
+ {label('topology.quiet_count', `${quiet} link without traffic`, { smart_count: quiet })}
353
+ </>}
169
354
  </Typography>
170
355
  {/* Saturation: the one signal a static shape cannot carry. */}
171
356
  {state.report.active > 0 && (
@@ -176,9 +361,8 @@ export function FougereTopology({ endpoint = CALL_ENDPOINT, fetcher = browserFet
176
361
  />
177
362
  )}
178
363
  </Row>
179
- <Box sx={{ display: 'grid', gap: 2, gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))' }}>
180
- {nodes.map((node) => <FrondCard key={node.frond} node={node} />)}
181
- </Box>
364
+ <Graph nodes={nodes} selected={shown?.frond} onSelect={setSelected} />
365
+ {shown && <Box sx={{ maxWidth: 520 }}><FrondCard node={shown} /></Box>}
182
366
  {/*
183
367
  An edge is only knowable on the side that made the call, so a process with no
184
368
  outgoing call has none — that is ordinary, not a gap in the reading.
package/src/topology.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  /** How the system is arranged, as the app itself reports it. */
2
+ import dagre from '@dagrejs/dagre';
2
3
  import {
3
4
  CALL_ENDPOINT,
4
5
  fetcher as browserFetcher,
@@ -7,13 +8,14 @@ import {
7
8
  } from '@fougere/app/client';
8
9
  import {
9
10
  ErrorCode,
11
+ type DeclaredEdge,
10
12
  type Edge,
11
13
  type FrondPlacement,
12
14
  type TopologyReport,
13
15
  Invocation,
14
16
  } from '@fougere/core/contract';
15
17
 
16
- export type { TopologyReport, FrondPlacement, Edge };
18
+ export type { TopologyReport, FrondPlacement, Edge, DeclaredEdge };
17
19
 
18
20
  /** The report, or `undefined` when the app serves no topology at all. */
19
21
  export async function fetchTopology(
@@ -39,21 +41,274 @@ export interface TopologyNode extends FrondPlacement {
39
41
  calls: Edge[];
40
42
  /** Fronds that called this one. */
41
43
  calledBy: Edge[];
44
+ /** Fronds its code reaches, whether or not a call has ever gone down the link. */
45
+ declaredCalls: DeclaredEdge[];
46
+ /** Where the config says to reach it, when it says so. */
47
+ at?: string;
48
+ /**
49
+ * Declared in `remotes:` and never heard from. A different absence from an opaque remote,
50
+ * which answered and keeps its shape to itself: this one may simply be down.
51
+ */
52
+ silent: boolean;
42
53
  }
43
54
 
44
- /** The report read as a graph, local fronds first. */
55
+ /** The report read as a graph — what runs here first, then what answered, then what never did. */
45
56
  export function nodesOf(report: TopologyReport): TopologyNode[] {
46
57
  const order = { local: 0, remote: 1 };
47
- return [...report.fronds]
58
+ const stated = new Map(report.declared.fronds.map((one) => [one.frond, one]));
59
+ const answered = new Set(report.fronds.map((frond) => frond.frond));
60
+
61
+ const around = (frond: string) => ({
62
+ calls: report.edges.filter((edge) => edge.from === frond),
63
+ calledBy: report.edges.filter((edge) => edge.to === frond),
64
+ declaredCalls: report.declared.edges.filter((edge) => edge.from === frond),
65
+ });
66
+
67
+ const heard: TopologyNode[] = [...report.fronds]
48
68
  .sort((a, b) => order[a.placement] - order[b.placement] || a.frond.localeCompare(b.frond))
49
69
  .map((frond) => ({
50
70
  ...frond,
51
- calls: report.edges.filter((edge) => edge.from === frond.frond),
52
- calledBy: report.edges.filter((edge) => edge.to === frond.frond),
71
+ ...around(frond.frond),
72
+ ...(stated.get(frond.frond)?.at ? { at: stated.get(frond.frond)!.at } : {}),
73
+ silent: false,
74
+ }));
75
+
76
+ // The whole reason the declared half travels: a frond that never answered is absent from
77
+ // the observed one, so without this the node the operator most needs to see is the one
78
+ // that disappears.
79
+ const silent: TopologyNode[] = report.declared.fronds
80
+ .filter((one) => one.placement === 'remote' && !answered.has(one.frond))
81
+ .sort((a, b) => a.frond.localeCompare(b.frond))
82
+ .map((one) => ({
83
+ frond: one.frond,
84
+ placement: 'remote' as const,
85
+ entities: 0,
86
+ doors: 0,
87
+ ...around(one.frond),
88
+ ...(one.at ? { at: one.at } : {}),
89
+ silent: true,
53
90
  }));
91
+
92
+ return [...heard, ...silent];
93
+ }
94
+
95
+ export interface Point {
96
+ x: number;
97
+ y: number;
98
+ }
99
+
100
+ /** A node placed on the drawing. `x`/`y` are its CENTRE, the convention the layout answers in. */
101
+ export interface PlacedNode {
102
+ node: TopologyNode;
103
+ x: number;
104
+ y: number;
105
+ width: number;
106
+ height: number;
107
+ }
108
+
109
+ /**
110
+ * One link between two nodes. `count` is absent on a link nothing has travelled: the config
111
+ * declares it and no call has gone down it, which is drawn broken rather than not drawn.
112
+ */
113
+ export interface PlacedEdge {
114
+ from: string;
115
+ to: string;
116
+ path: string;
117
+ /** Where this link's figure goes — clear of every node, or absent when the route is a point. */
118
+ at?: Point;
119
+ count?: number;
120
+ errors: number;
121
+ /** Where this link's volume sits among the others, `0` to `1`. Absent when nothing travelled. */
122
+ weight?: number;
123
+ }
124
+
125
+ export interface Drawing {
126
+ nodes: PlacedNode[];
127
+ edges: PlacedEdge[];
128
+ width: number;
129
+ height: number;
130
+ }
131
+
132
+ const NODE = { width: 176, height: 54 };
133
+
134
+ /**
135
+ * Where each frond sits, and the line between two of them.
136
+ *
137
+ * The placement is `@dagrejs/dagre`'s — ranks by depth, order within a rank chosen to cross as
138
+ * little as possible, and a polyline per edge that goes around the nodes between its ends. Two
139
+ * hand-written columns could not do the second or the third: a chain of three fronds put two of
140
+ * them in one column, and the curve between them had to bulge around whatever sat in the way.
141
+ *
142
+ * Rendering stays ours, which is why only the layout is bought: the drawing reads `error.main`
143
+ * and the rest of the MUI palette off the theme, and a library that painted its own canvas
144
+ * would have to be told the theme again on every switch. Kiali left Cytoscape for this in 2.0.0,
145
+ * and every service map whose source can be read — Grafana, Jaeger, Linkerd — buys the same half.
146
+ */
147
+ export function layoutOf(nodes: readonly TopologyNode[]): Drawing {
148
+ const graph = new dagre.graphlib.Graph();
149
+ // Left to right: the reader is the process being asked, and what it depends on goes outward.
150
+ graph.setGraph({ rankdir: 'LR', nodesep: 26, ranksep: 92, marginx: 22, marginy: 22 });
151
+ graph.setDefaultEdgeLabel(() => ({}));
152
+
153
+ for (const node of nodes) graph.setNode(node.frond, { ...NODE });
154
+
155
+ const links = linksBetween(nodes);
156
+ for (const link of links) {
157
+ if (graph.hasNode(link.from) && graph.hasNode(link.to)) graph.setEdge(link.from, link.to);
158
+ }
159
+
160
+ dagre.layout(graph);
161
+
162
+ const travelled = links.map((link) => link.count).filter((count): count is number => count !== undefined);
163
+ const shape = graph.graph();
164
+
165
+ const placed: PlacedNode[] = nodes
166
+ .filter((node) => graph.hasNode(node.frond))
167
+ .map((node) => {
168
+ const { x, y, width, height } = graph.node(node.frond) as PlacedNode;
169
+
170
+ return { node, x, y, width, height };
171
+ });
172
+
173
+ return {
174
+ nodes: placed,
175
+ edges: links.flatMap((link) => {
176
+ const drawn = graph.hasNode(link.from) && graph.hasNode(link.to) ? graph.edge(link.from, link.to) : undefined;
177
+ if (!drawn) return [];
178
+
179
+ const points = drawn.points as Point[];
180
+ const at = labelAt(points, placed, figureOf(link).length * 3.4 + 8);
181
+
182
+ return [{
183
+ ...link,
184
+ path: through(points),
185
+ ...(at ? { at } : {}),
186
+ ...(link.count === undefined ? {} : { weight: weightOf(link.count, travelled) }),
187
+ }];
188
+ }),
189
+ width: shape.width ?? 0,
190
+ height: shape.height ?? 0,
191
+ };
192
+ }
193
+
194
+ /** A count as an operator reads it at a glance — `2.1k`, never `2140`. */
195
+ export function short(count: number): string {
196
+ if (count < 1000) return String(count);
197
+
198
+ return `${(count / 1000).toFixed(count < 10_000 ? 1 : 0)}k`;
199
+ }
200
+
201
+ /** What a link says of itself, and the only figure the drawing carries. */
202
+ export function figureOf(link: Pick<PlacedEdge, 'count' | 'errors'>): string {
203
+ if (link.count === undefined) return '—';
204
+
205
+ return link.errors > 0 ? `${short(link.count)} · ${short(link.errors)} ✕` : short(link.count);
206
+ }
207
+
208
+ /**
209
+ * Where a volume sits among the others, on a SQUARE ROOT scale. Linear makes every link but the
210
+ * busiest a hairline; a log flattens them the other way, and flattening is wrong at this spread —
211
+ * 40 to 2140 is under two orders of magnitude, and the busiest link deserves to look it.
212
+ */
213
+ function weightOf(count: number, travelled: readonly number[]): number {
214
+ const high = Math.max(...travelled);
215
+ if (high <= 0) return 1;
216
+
217
+ return Math.sqrt(count / high);
218
+ }
219
+
220
+ /** Every link, observed or merely declared, each named once. */
221
+ function linksBetween(nodes: readonly TopologyNode[]): Omit<PlacedEdge, 'path'>[] {
222
+ const observed = nodes.flatMap((one) => one.calls);
223
+ const travelled = new Set(observed.map((edge) => `${edge.from} ${edge.to}`));
224
+
225
+ return [
226
+ ...observed.map((edge) => ({ from: edge.from, to: edge.to, count: edge.count, errors: edge.errors })),
227
+ ...nodes
228
+ .flatMap((one) => one.declaredCalls)
229
+ .filter((edge) => !travelled.has(`${edge.from} ${edge.to}`))
230
+ .map((edge) => ({ from: edge.from, to: edge.to, errors: 0 })),
231
+ ];
232
+ }
233
+
234
+ /**
235
+ * A B-spline over the waypoints — what dagre-d3 and Grafana draw their edges with.
236
+ *
237
+ * It deliberately does NOT pass through them. A curve pinned to the points takes its character
238
+ * from their COUNT, and a router hands back two for a short hop and five for one that goes around
239
+ * something — so every edge came out with a bearing of its own and the drawing read as noise.
240
+ *
241
+ * The ends are TRIPLED rather than joined by a straight segment: a clamped spline reaches its
242
+ * endpoint on its own tangent, where an anchor line meets the curve at whatever angle it happens
243
+ * to, which is a visible corner exactly where the arrow is.
244
+ */
245
+ function through(points: readonly Point[]): string {
246
+ const first = points[0];
247
+ const last = points.at(-1);
248
+ if (!first || !last) return '';
249
+ if (points.length === 1) return `M ${first.x} ${first.y}`;
250
+
251
+ const held = [first, first, ...points, last, last];
252
+ const steps = held.slice(2).map((point, nth) => {
253
+ const back = held[nth]!;
254
+ const near = held[nth + 1]!;
255
+
256
+ return `C ${(2 * back.x + near.x) / 3} ${(2 * back.y + near.y) / 3},`
257
+ + ` ${(back.x + 2 * near.x) / 3} ${(back.y + 2 * near.y) / 3},`
258
+ + ` ${(back.x + 4 * near.x + point.x) / 6} ${(back.y + 4 * near.y + point.y) / 6}`;
259
+ });
260
+
261
+ return `M ${first.x} ${first.y} ${steps.join(' ')}`;
262
+ }
263
+
264
+ /** A point at a fraction of a route's length. */
265
+ function alongOf(points: readonly Point[], fraction: number): Point | undefined {
266
+ if (fraction <= 0 || fraction >= 1) return undefined;
267
+ const spans = points.slice(1).map((point, nth) => Math.hypot(point.x - points[nth]!.x, point.y - points[nth]!.y));
268
+ const target = spans.reduce((sum, span) => sum + span, 0) * fraction;
269
+
270
+ let walked = 0;
271
+ for (let nth = 0; nth < spans.length; nth++) {
272
+ if (walked + spans[nth]! >= target) {
273
+ const into = spans[nth] === 0 ? 0 : (target - walked) / spans[nth]!;
274
+
275
+ return {
276
+ x: points[nth]!.x + (points[nth + 1]!.x - points[nth]!.x) * into,
277
+ y: points[nth]!.y + (points[nth + 1]!.y - points[nth]!.y) * into,
278
+ };
279
+ }
280
+ walked += spans[nth]!;
281
+ }
282
+
283
+ return points.at(-1);
284
+ }
285
+
286
+ /**
287
+ * Where a link's figure sits: halfway ALONG the route, then moved along it until it sits inside no
288
+ * node. The middle waypoint is not the middle of the line — a routed edge bunches its points near
289
+ * the ends — and a figure drawn over a card cannot be read at all.
290
+ */
291
+ function labelAt(points: readonly Point[], boxes: readonly PlacedNode[], half: number): Point | undefined {
292
+ const inside = (point: Point) => boxes.some((box) =>
293
+ Math.abs(point.x - box.x) < box.width / 2 + half && Math.abs(point.y - box.y) < box.height / 2 + 13);
294
+
295
+ const middle = alongOf(points, 0.5);
296
+ if (!middle || !inside(middle)) return middle;
297
+
298
+ for (let step = 1; step <= 12; step++) {
299
+ for (const side of [-1, 1]) {
300
+ const moved = alongOf(points, 0.5 + side * step * 0.035);
301
+ if (moved && !inside(moved)) return moved;
302
+ }
303
+ }
304
+
305
+ return middle;
54
306
  }
55
307
 
56
- /** A frond this process called that publishes nothing of its own shape. */
308
+ /**
309
+ * A frond this process called that publishes nothing of its own shape. Read AFTER `silent`:
310
+ * a frond that never answered has the same two zeroes and a different reason for them.
311
+ */
57
312
  export function isOpaque(node: FrondPlacement): boolean {
58
313
  return node.placement === 'remote' && node.entities === 0 && node.doors === 0;
59
314
  }