@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.
- package/dist/topology-page.d.ts.map +1 -1
- package/dist/topology-page.js +105 -11
- package/dist/topology-page.js.map +1 -1
- package/dist/topology.d.ts +67 -5
- package/dist/topology.d.ts.map +1 -1
- package/dist/topology.js +188 -5
- package/dist/topology.js.map +1 -1
- package/package.json +5 -4
- package/src/topology-page.tsx +200 -16
- package/src/topology.ts +261 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"topology-page.d.ts","sourceRoot":"","sources":["../src/topology-page.tsx"],"names":[],"mappings":"
|
|
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"}
|
package/dist/topology-page.js
CHANGED
|
@@ -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,
|
|
9
|
-
* is
|
|
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:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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":"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"}
|
|
1
|
+
{"version":3,"file":"topology-page.js","sourceRoot":"","sources":["../src/topology-page.tsx"],"names":[],"mappings":"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"}
|
package/dist/topology.d.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
package/dist/topology.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"topology.d.ts","sourceRoot":"","sources":["../src/topology.ts"],"names":[],"mappings":"
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
/**
|
|
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
|
}
|
package/dist/topology.js.map
CHANGED
|
@@ -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,
|
|
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.
|
|
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
|
-
"@
|
|
38
|
-
"@fougere/
|
|
39
|
-
"@fougere/
|
|
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",
|
package/src/topology-page.tsx
CHANGED
|
@@ -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,
|
|
9
|
-
* is
|
|
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={
|
|
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
|
-
{
|
|
84
|
-
? label('topology.
|
|
85
|
-
:
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
180
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
/**
|
|
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
|
}
|