ankka-console 0.10.0 → 0.11.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/client/control-plane.d.ts +25 -1
- package/dist/client/control-plane.js +60 -1
- package/dist/client/schemas.d.ts +525 -0
- package/dist/client/schemas.js +174 -0
- package/dist/context.d.ts +98 -0
- package/dist/context.js +73 -0
- package/dist/extensions/render.d.ts +2 -0
- package/dist/extensions/render.js +5 -0
- package/dist/extensions/types.d.ts +1 -1
- package/dist/extensions/types.js +7 -0
- package/dist/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
- package/dist/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +7 -1
- package/dist/routes/auth.sign-out.js +8 -3
- package/dist/routes/front.js +12 -6
- package/dist/routes/logs.d.ts +35 -0
- package/dist/routes/logs.js +15 -14
- package/dist/routes/members.js +11 -8
- package/dist/routes/organization-new.js +7 -4
- package/dist/routes/organization.js +13 -6
- package/dist/routes/project-new.js +8 -4
- package/dist/routes/project.d.ts +73 -2
- package/dist/routes/project.js +81 -9
- package/dist/routes/service-apply.d.ts +5 -2
- package/dist/routes/service-apply.js +17 -14
- package/dist/routes/service-history.d.ts +30 -0
- package/dist/routes/service-history.js +79 -0
- package/dist/routes/service-topology.d.ts +153 -0
- package/dist/routes/service-topology.js +54 -0
- package/dist/routes/service.d.ts +34 -10
- package/dist/routes/service.js +27 -22
- package/dist/routes/stream.service.js +18 -1
- package/dist/routes/tokens.js +8 -6
- package/dist/routes.js +2 -0
- package/dist/styles.css +2 -481
- package/dist/testing/fake-control-plane.d.ts +155 -0
- package/dist/testing/fake-control-plane.js +379 -12
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/scenarios.js +6 -0
- package/dist/theme.css +66 -0
- package/dist/ui/console.d.ts +19 -6
- package/dist/ui/console.js +9 -2
- package/dist/ui/errors.js +7 -5
- package/dist/ui/hosting.d.ts +11 -0
- package/dist/ui/hosting.js +20 -0
- package/dist/ui/primitives/button.d.ts +11 -0
- package/dist/ui/primitives/button.js +21 -0
- package/dist/ui/primitives/select.d.ts +10 -0
- package/dist/ui/primitives/select.js +4 -0
- package/dist/ui/primitives/switch.d.ts +9 -0
- package/dist/ui/primitives/switch.js +4 -0
- package/dist/ui/shape.d.ts +4 -0
- package/dist/ui/shape.js +36 -0
- package/dist/ui/shell.d.ts +76 -0
- package/dist/ui/shell.js +107 -0
- package/dist/ui/tokens.d.ts +82 -0
- package/dist/ui/tokens.js +172 -0
- package/dist/ui/topology/Graph.d.ts +13 -0
- package/dist/ui/topology/Graph.js +56 -0
- package/dist/ui/topology/Table.d.ts +10 -0
- package/dist/ui/topology/Table.js +14 -0
- package/dist/ui/topology/layout.d.ts +113 -0
- package/dist/ui/topology/layout.js +177 -0
- package/dist/ui/use-stream.d.ts +137 -1
- package/dist/ui/use-stream.js +11 -1
- package/package.json +11 -3
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The console's look as values: the one source the stylesheet is generated from, the contrast test
|
|
3
|
+
* reads, and the documentation's table of properties is written from. Each becomes a `--ac-*`
|
|
4
|
+
* custom property a host may set; the names are a contract, the values are not.
|
|
5
|
+
*
|
|
6
|
+
* Text sits on translucent surfaces, so its contrast depends on what is behind it. The backdrop is
|
|
7
|
+
* brightest where the glow peaks, and every ink must keep 4.5:1 on every surface there, in both
|
|
8
|
+
* themes. The glow's alpha is therefore a limit: `test/contrast.test.ts` fails if it is raised past
|
|
9
|
+
* what the inks can stand.
|
|
10
|
+
*/
|
|
11
|
+
export const tokens = {
|
|
12
|
+
dark: {
|
|
13
|
+
mesh: {
|
|
14
|
+
base: "#111f26",
|
|
15
|
+
slate: "#1e2e3a",
|
|
16
|
+
glow: "rgba(212, 128, 88, 0.34)",
|
|
17
|
+
peach: "rgba(222, 170, 132, 0.16)",
|
|
18
|
+
dot: "rgba(255, 255, 255, 0.08)",
|
|
19
|
+
},
|
|
20
|
+
surface: {
|
|
21
|
+
glass: "rgba(255, 255, 255, 0.05)",
|
|
22
|
+
glassStrong: "rgba(255, 255, 255, 0.09)",
|
|
23
|
+
glassLine: "rgba(255, 255, 255, 0.2)",
|
|
24
|
+
tile: "rgba(255, 255, 255, 0.06)",
|
|
25
|
+
tileLine: "rgba(255, 255, 255, 0.14)",
|
|
26
|
+
node: "rgba(255, 255, 255, 0.1)",
|
|
27
|
+
nodeLine: "rgba(255, 255, 255, 0.38)",
|
|
28
|
+
inset: "rgba(0, 0, 0, 0.2)",
|
|
29
|
+
edge: "rgba(255, 255, 255, 0.7)",
|
|
30
|
+
},
|
|
31
|
+
ink: { ink: "#f5f8f7", ink2: "#dfe8e6", ink3: "#ccd9d6" },
|
|
32
|
+
accent: { amber: "#f0b83c", amberInk: "#13343b", green: "#8fe0ad", red: "#ffc4bc", redWash: "rgba(255, 138, 128, 0.16)" },
|
|
33
|
+
shadow: "0 18px 50px rgba(5, 15, 18, 0.28)",
|
|
34
|
+
},
|
|
35
|
+
light: {
|
|
36
|
+
mesh: {
|
|
37
|
+
base: "#e6eeee",
|
|
38
|
+
slate: "#cfe0dc",
|
|
39
|
+
glow: "rgba(224, 165, 38, 0.30)",
|
|
40
|
+
peach: "rgba(241, 213, 194, 0.80)",
|
|
41
|
+
dot: "rgba(19, 52, 59, 0.10)",
|
|
42
|
+
},
|
|
43
|
+
surface: {
|
|
44
|
+
glass: "rgba(255, 255, 255, 0.50)",
|
|
45
|
+
glassStrong: "rgba(255, 255, 255, 0.85)",
|
|
46
|
+
glassLine: "rgba(19, 52, 59, 0.14)",
|
|
47
|
+
tile: "rgba(255, 255, 255, 0.50)",
|
|
48
|
+
tileLine: "rgba(19, 52, 59, 0.10)",
|
|
49
|
+
node: "rgba(255, 255, 255, 0.62)",
|
|
50
|
+
nodeLine: "rgba(19, 52, 59, 0.22)",
|
|
51
|
+
inset: "rgba(19, 52, 59, 0.06)",
|
|
52
|
+
edge: "rgba(19, 52, 59, 0.55)",
|
|
53
|
+
},
|
|
54
|
+
ink: { ink: "#13343b", ink2: "#33504f", ink3: "#4a6465" },
|
|
55
|
+
accent: { amber: "#e0a526", amberInk: "#13343b", green: "#1f6b3a", red: "#a8231b", redWash: "#fbe9e7" },
|
|
56
|
+
shadow: "0 18px 50px rgba(19, 52, 59, 0.10)",
|
|
57
|
+
},
|
|
58
|
+
radius: { panel: "20px", card: "14px", control: "10px", node: "18px" },
|
|
59
|
+
font: {
|
|
60
|
+
sans: '"Inter Variable", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
|
|
61
|
+
mono: 'ui-monospace, "SF Mono", Menlo, Consolas, monospace',
|
|
62
|
+
},
|
|
63
|
+
size: { rail: "56px", panel: "232px", inspector: "300px", bar: "64px" },
|
|
64
|
+
};
|
|
65
|
+
/** Every colour property of a theme, by its CSS name without the `--ac-` prefix. */
|
|
66
|
+
export function colours(theme) {
|
|
67
|
+
return [
|
|
68
|
+
["color-mesh-base", theme.mesh.base],
|
|
69
|
+
["color-mesh-slate", theme.mesh.slate],
|
|
70
|
+
["color-glow", theme.mesh.glow],
|
|
71
|
+
["color-peach", theme.mesh.peach],
|
|
72
|
+
["color-dot", theme.mesh.dot],
|
|
73
|
+
["color-glass", theme.surface.glass],
|
|
74
|
+
["color-glass-strong", theme.surface.glassStrong],
|
|
75
|
+
["color-glass-line", theme.surface.glassLine],
|
|
76
|
+
["color-tile", theme.surface.tile],
|
|
77
|
+
["color-tile-line", theme.surface.tileLine],
|
|
78
|
+
["color-node", theme.surface.node],
|
|
79
|
+
["color-node-line", theme.surface.nodeLine],
|
|
80
|
+
["color-inset", theme.surface.inset],
|
|
81
|
+
["color-edge", theme.surface.edge],
|
|
82
|
+
["color-ink", theme.ink.ink],
|
|
83
|
+
["color-ink-2", theme.ink.ink2],
|
|
84
|
+
["color-ink-3", theme.ink.ink3],
|
|
85
|
+
["color-amber", theme.accent.amber],
|
|
86
|
+
["color-amber-ink", theme.accent.amberInk],
|
|
87
|
+
["color-green", theme.accent.green],
|
|
88
|
+
["color-red", theme.accent.red],
|
|
89
|
+
["color-red-wash", theme.accent.redWash],
|
|
90
|
+
["shadow-part", theme.shadow],
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
/** The properties that are the same in both themes. */
|
|
94
|
+
export function constants(t) {
|
|
95
|
+
return [
|
|
96
|
+
["radius-panel", t.radius.panel],
|
|
97
|
+
["radius-card", t.radius.card],
|
|
98
|
+
["radius-control", t.radius.control],
|
|
99
|
+
["radius-node", t.radius.node],
|
|
100
|
+
["font-sans", t.font.sans],
|
|
101
|
+
["font-mono", t.font.mono],
|
|
102
|
+
["size-rail", t.size.rail],
|
|
103
|
+
["size-panel", t.size.panel],
|
|
104
|
+
["size-inspector", t.size.inspector],
|
|
105
|
+
["size-bar", t.size.bar],
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The stylesheet's theme. The console is dark; its values are the theme itself. Light redefines the
|
|
110
|
+
* same properties when a host chooses it, by putting `ac-light` on the shell. Browsers report a
|
|
111
|
+
* preference for light when nobody has stated one, so the browser's preference is not followed.
|
|
112
|
+
* Both sit on `:root` and inside the `theme` layer, so a host's own, unlayered declaration of any of
|
|
113
|
+
* them wins in either theme, on any element.
|
|
114
|
+
*
|
|
115
|
+
* Tailwind prefixes the `@theme` names (`--color-ink` becomes `--ac-color-ink`); the light block is
|
|
116
|
+
* plain CSS and names the prefixed property itself.
|
|
117
|
+
*/
|
|
118
|
+
export function toCss(t) {
|
|
119
|
+
const decl = (pairs, prefix) => pairs.map(([k, v]) => ` ${prefix}${k}: ${v};`).join("\n");
|
|
120
|
+
return [
|
|
121
|
+
"/* Generated from src/ui/tokens.ts by scripts/build.ts. Do not edit. */",
|
|
122
|
+
"@theme static {",
|
|
123
|
+
" --*: initial;",
|
|
124
|
+
" --spacing: 0.25rem;",
|
|
125
|
+
decl(colours(t.dark), "--"),
|
|
126
|
+
decl(constants(t), "--"),
|
|
127
|
+
"}",
|
|
128
|
+
"",
|
|
129
|
+
"@layer theme {",
|
|
130
|
+
" :root:has(.ac-light) {",
|
|
131
|
+
decl(colours(t.light), " --ac-"),
|
|
132
|
+
" }",
|
|
133
|
+
"}",
|
|
134
|
+
"",
|
|
135
|
+
].join("\n");
|
|
136
|
+
}
|
|
137
|
+
/** What each property is for, by its name without the `--ac-` prefix: the documentation's table. */
|
|
138
|
+
export const roles = {
|
|
139
|
+
"color-mesh-base": "The backdrop's darker colour, and what a surface is drawn over where blur is unavailable.",
|
|
140
|
+
"color-mesh-slate": "The backdrop's lighter colour.",
|
|
141
|
+
"color-glow": "The glow at the centre of the screen. Its alpha is the brightest the backdrop gets: the limit.",
|
|
142
|
+
"color-peach": "The warmth in the lower corner.",
|
|
143
|
+
"color-dot": "The dot grid across the backdrop.",
|
|
144
|
+
"color-glass": "The rail, the bar, the listing and the inspector.",
|
|
145
|
+
"color-glass-strong": "A button, and the item being read in the listing.",
|
|
146
|
+
"color-glass-line": "The edge of the glass surfaces.",
|
|
147
|
+
"color-tile": "A card on the page, and an item in the listing.",
|
|
148
|
+
"color-tile-line": "A card's edge and the rules in its tables.",
|
|
149
|
+
"color-node": "A part of a service's shape and of its topology.",
|
|
150
|
+
"color-node-line": "A part's edge.",
|
|
151
|
+
"color-inset": "Fields, logs, the section links and a part's inset row.",
|
|
152
|
+
"color-edge": "The joins between parts.",
|
|
153
|
+
"color-ink": "Text.",
|
|
154
|
+
"color-ink-2": "Secondary text: labels, hints, counts.",
|
|
155
|
+
"color-ink-3": "Section titles and placeholders.",
|
|
156
|
+
"color-amber": "The accent: focus, the current area, toggles, the primary operation.",
|
|
157
|
+
"color-amber-ink": "Text on the accent.",
|
|
158
|
+
"color-green": "A ready lifecycle's mark.",
|
|
159
|
+
"color-red": "Failure: a failed lifecycle, a destructive button, a field's error.",
|
|
160
|
+
"color-red-wash": "Behind a refusal.",
|
|
161
|
+
"shadow-part": "The shadow under a part.",
|
|
162
|
+
"radius-panel": "The shell's surfaces.",
|
|
163
|
+
"radius-card": "Cards.",
|
|
164
|
+
"radius-control": "Fields and listing items.",
|
|
165
|
+
"radius-node": "A part of the shape.",
|
|
166
|
+
"font-sans": "The face; the package serves Inter itself.",
|
|
167
|
+
"font-mono": "Logs, descriptors and secrets.",
|
|
168
|
+
"size-rail": "The rail's width.",
|
|
169
|
+
"size-panel": "The listing's width.",
|
|
170
|
+
"size-inspector": "The inspector's width.",
|
|
171
|
+
"size-bar": "The bar's height.",
|
|
172
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A topology as a picture: a column per layer, declared connections solid, observed calls dashed and
|
|
3
|
+
* heavier the more they were made, a warning mark only for failed or unanswered calls. The picture
|
|
4
|
+
* says what it holds in its label; the same facts are in the table beside it.
|
|
5
|
+
*/
|
|
6
|
+
import { type View } from "./layout.ts";
|
|
7
|
+
export declare function Graph({ shown, differs, selected, onSelect, }: {
|
|
8
|
+
shown: View;
|
|
9
|
+
/** Nodes not on every instance that answered. */
|
|
10
|
+
differs: Set<string>;
|
|
11
|
+
selected: string | null;
|
|
12
|
+
onSelect: (id: string) => void;
|
|
13
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* A topology as a picture: a column per layer, declared connections solid, observed calls dashed and
|
|
4
|
+
* heavier the more they were made, a warning mark only for failed or unanswered calls. The picture
|
|
5
|
+
* says what it holds in its label; the same facts are in the table beside it.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, edgeMark, weight } from "./layout.js";
|
|
8
|
+
const GRID = { margin: 16, column: 210, row: 64, width: 170, height: 40 };
|
|
9
|
+
function clip(text, length) {
|
|
10
|
+
return text.length > length ? text.slice(0, length - 1) + "…" : text;
|
|
11
|
+
}
|
|
12
|
+
function kindInWords(kind) {
|
|
13
|
+
return kind.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
export function Graph({ shown, differs, selected, onSelect, }) {
|
|
16
|
+
const columns = Math.max(0, ...shown.nodes.map((n) => n.column)) + 1;
|
|
17
|
+
const rows = Math.max(0, ...shown.nodes.map((n) => n.row)) + 1;
|
|
18
|
+
const width = GRID.margin * 2 + (columns - 1) * GRID.column + GRID.width;
|
|
19
|
+
const height = GRID.margin * 2 + (rows - 1) * GRID.row + GRID.height;
|
|
20
|
+
const at = new Map(shown.nodes.map((n) => [n.id, { x: GRID.margin + n.column * GRID.column, y: GRID.margin + n.row * GRID.row }]));
|
|
21
|
+
const path = (from, to, below) => {
|
|
22
|
+
const a = at.get(from);
|
|
23
|
+
const b = at.get(to);
|
|
24
|
+
if (!a || !b || from === to)
|
|
25
|
+
return null;
|
|
26
|
+
const forwards = b.x >= a.x;
|
|
27
|
+
const x1 = forwards ? a.x + GRID.width : a.x;
|
|
28
|
+
const x2 = forwards ? b.x : b.x + GRID.width;
|
|
29
|
+
const y1 = a.y + GRID.height / 2 + below;
|
|
30
|
+
const y2 = b.y + GRID.height / 2 + below;
|
|
31
|
+
const bend = (x2 - x1) / 2;
|
|
32
|
+
return { d: `M ${x1} ${y1} C ${x1 + bend} ${y1}, ${x2 - bend} ${y2}, ${x2} ${y2}`, mx: (x1 + x2) / 2, my: (y1 + y2) / 2 };
|
|
33
|
+
};
|
|
34
|
+
const node = (n) => {
|
|
35
|
+
const { x, y } = at.get(n.id);
|
|
36
|
+
const outside = n.kind.startsWith("External") || n.kind === "UnknownCaller";
|
|
37
|
+
const classes = ["ac-topology-node", outside ? "ac-topology-outside" : "", n.platform ? "ac-topology-platform" : "", differs.has(n.id) ? "ac-topology-differs" : "", n.id === selected ? "ac-topology-selected" : ""]
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.join(" ");
|
|
40
|
+
const kind = kindInWords(n.kind) + (n.platform ? ", platform" : "") + (differs.has(n.id) ? ", not on every instance" : "");
|
|
41
|
+
return (_jsxs("g", { className: classes, transform: `translate(${x} ${y})`, tabIndex: 0, role: "button", "aria-label": `${n.label}: ${kind}`, onClick: () => onSelect(n.id), onKeyDown: (e) => {
|
|
42
|
+
if (e.key === "Enter" || e.key === " ")
|
|
43
|
+
onSelect(n.id);
|
|
44
|
+
}, children: [_jsx("rect", { width: GRID.width, height: GRID.height, rx: 12, filter: "url(#ac-topology-shadow)" }), _jsx("text", { x: 10, y: 16, className: "ac-topology-name", children: clip(n.label, 22) }), _jsx("text", { x: 10, y: 30, className: "ac-topology-kind", children: clip(kind, 28) })] }, n.id));
|
|
45
|
+
};
|
|
46
|
+
return (_jsxs("svg", { className: "ac-topology-graph", viewBox: `0 0 ${width} ${height}`, width: width, height: height, role: "group", "aria-label": describe(shown), children: [_jsx("defs", { children: _jsx("filter", { id: "ac-topology-shadow", x: "-20%", y: "-30%", width: "140%", height: "180%", children: _jsx("feDropShadow", { dx: "0", dy: "6", stdDeviation: "8", floodColor: "#05090b", floodOpacity: "0.28" }) }) }), shown.declared.map((e) => {
|
|
47
|
+
const p = path(e.from, e.to, 0);
|
|
48
|
+
return p ? (_jsx("path", { className: "ac-topology-declared", d: p.d, children: _jsx("title", { children: `${e.from} to ${e.to}: ${e.kind}` }) }, `d:${e.from}>${e.to}:${e.kind}`)) : null;
|
|
49
|
+
}), shown.calls.map((c) => {
|
|
50
|
+
const p = path(c.from, c.to, 7);
|
|
51
|
+
if (!p)
|
|
52
|
+
return null;
|
|
53
|
+
const warning = edgeMark(c.pairs) === "warning";
|
|
54
|
+
return (_jsxs("g", { children: [_jsx("path", { className: `ac-topology-call${warning ? " ac-topology-warning" : ""}`, d: p.d, style: { strokeWidth: `${0.75 + weight(c.pairs) * 0.75}px` }, children: _jsx("title", { children: `${c.from} calls ${c.to}` }) }), warning ? (_jsx("text", { className: "ac-topology-mark", x: p.mx - 3, y: p.my - 4, children: "!" })) : null] }, `c:${c.from}>${c.to}`));
|
|
55
|
+
}), shown.nodes.map(node)] }));
|
|
56
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The topology as text: every fact the picture shows, in tables a screen reader and a search can
|
|
3
|
+
* read. Handled and unanswered are separate columns, because they are separate facts.
|
|
4
|
+
*/
|
|
5
|
+
import type { Pair, View } from "./layout.ts";
|
|
6
|
+
export declare function duration(pair: Pair): string;
|
|
7
|
+
export declare function Table({ shown, differs }: {
|
|
8
|
+
shown: View;
|
|
9
|
+
differs: Map<string, string[]>;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
function nameOf(shown, id) {
|
|
3
|
+
return shown.nodes.find((n) => n.id === id)?.label ?? id;
|
|
4
|
+
}
|
|
5
|
+
function kindInWords(kind) {
|
|
6
|
+
return kind.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
export function duration(pair) {
|
|
9
|
+
const d = pair.durationMillis;
|
|
10
|
+
return d ? `~${d.p50} / ~${d.p99} / ~${d.max} ms` : "";
|
|
11
|
+
}
|
|
12
|
+
export function Table({ shown, differs }) {
|
|
13
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", "aria-label": "Components", children: [_jsx("caption", { className: "ac-visually-hidden", children: "Components" }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Name" }), _jsx("th", { scope: "col", children: "Kind" }), _jsx("th", { scope: "col", children: "Handlers" })] }) }), _jsx("tbody", { children: shown.nodes.map((n) => (_jsxs("tr", { "data-node": n.id, children: [_jsx("td", { children: n.label }), _jsxs("td", { children: [kindInWords(n.kind), n.platform ? ", platform" : "", differs.has(n.id) ? `, only on ${differs.get(n.id).join(", ")}` : ""] }), _jsx("td", { children: n.handlers.map((h) => h.name).join(", ") })] }, n.id))) })] }) }), shown.declared.length > 0 ? (_jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", "aria-label": "Declared connections", children: [_jsx("caption", { className: "ac-visually-hidden", children: "Declared connections" }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "From" }), _jsx("th", { scope: "col", children: "To" }), _jsx("th", { scope: "col", children: "As" })] }) }), _jsx("tbody", { children: shown.declared.map((e) => (_jsxs("tr", { children: [_jsx("td", { children: nameOf(shown, e.from) }), _jsx("td", { children: nameOf(shown, e.to) }), _jsx("td", { children: e.kind })] }, `${e.from}>${e.to}:${e.kind}`))) })] }) })) : null, shown.calls.length > 0 ? (_jsx("div", { className: "ac-table-wrap", children: _jsxs("table", { className: "ac-table", "aria-label": "Observed calls", children: [_jsx("caption", { className: "ac-visually-hidden", children: "Observed calls" }), _jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "From" }), _jsx("th", { scope: "col", children: "To" }), _jsx("th", { scope: "col", children: "Handler to handler" }), _jsx("th", { scope: "col", className: "ac-num", children: "Ok" }), _jsx("th", { scope: "col", className: "ac-num", children: "Refused" }), _jsx("th", { scope: "col", className: "ac-num", children: "Failed" }), _jsx("th", { scope: "col", className: "ac-num", children: "Timed out" }), _jsx("th", { scope: "col", className: "ac-num", children: "Undelivered" }), _jsx("th", { scope: "col", children: "p50 / p99 / max" })] }) }), _jsx("tbody", { children: shown.calls.flatMap((c) => c.pairs.map((p) => (_jsxs("tr", { "data-pair": `${p.caller} -> ${p.callee}`, children: [_jsx("td", { children: nameOf(shown, c.from) }), _jsx("td", { children: nameOf(shown, c.to) }), _jsxs("td", { children: [p.caller, " \u2192 ", p.callee, p.streaming ? " (stream)" : ""] }), _jsx("td", { className: "ac-num", children: p.handled?.ok ?? 0 }), _jsx("td", { className: "ac-num", children: p.handled?.refused ?? 0 }), _jsx("td", { className: "ac-num", children: p.handled?.failed ?? 0 }), _jsx("td", { className: "ac-num", children: p.unanswered?.timedOut ?? 0 }), _jsx("td", { className: "ac-num", children: p.unanswered?.undelivered ?? 0 }), _jsx("td", { children: duration(p) })] }, `${c.from}>${c.to}:${p.caller}>${p.callee}`)))) })] }) })) : null] }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rules for reading a topology: what is shown, what is left out, and where each thing goes.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions, document in and plain data out. The local console applies the same rules
|
|
5
|
+
* (`cli/src/main/resources/console/topology.js`), and both are tested against every fixture in
|
|
6
|
+
* `console/package/fixtures/topology/`, so the two consoles cannot disagree about a rule without one
|
|
7
|
+
* suite failing. Browser-safe: nothing here imports a Node module or the server entry point.
|
|
8
|
+
*/
|
|
9
|
+
export interface Handled {
|
|
10
|
+
ok?: number;
|
|
11
|
+
refused?: number;
|
|
12
|
+
failed?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface Unanswered {
|
|
15
|
+
timedOut?: number;
|
|
16
|
+
undelivered?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface Pair {
|
|
19
|
+
caller: string;
|
|
20
|
+
callee: string;
|
|
21
|
+
handled?: Handled;
|
|
22
|
+
unanswered?: Unanswered;
|
|
23
|
+
durationMillis?: {
|
|
24
|
+
p50: number;
|
|
25
|
+
p99: number;
|
|
26
|
+
max: number;
|
|
27
|
+
bucketed?: boolean;
|
|
28
|
+
};
|
|
29
|
+
streaming?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface Node {
|
|
32
|
+
id: string;
|
|
33
|
+
kind: string;
|
|
34
|
+
layer: number;
|
|
35
|
+
platform: boolean;
|
|
36
|
+
handlers: {
|
|
37
|
+
name: string;
|
|
38
|
+
type: string;
|
|
39
|
+
streaming?: boolean | null;
|
|
40
|
+
}[];
|
|
41
|
+
}
|
|
42
|
+
export interface Declared {
|
|
43
|
+
from: string;
|
|
44
|
+
to: string;
|
|
45
|
+
kind: string;
|
|
46
|
+
}
|
|
47
|
+
export interface Call {
|
|
48
|
+
from: string;
|
|
49
|
+
to: string;
|
|
50
|
+
pairs: Pair[];
|
|
51
|
+
}
|
|
52
|
+
export interface Window {
|
|
53
|
+
seconds: number;
|
|
54
|
+
since: string;
|
|
55
|
+
}
|
|
56
|
+
export interface Document {
|
|
57
|
+
nodes?: Node[];
|
|
58
|
+
declared?: Declared[];
|
|
59
|
+
calls?: Call[];
|
|
60
|
+
window?: Window;
|
|
61
|
+
}
|
|
62
|
+
export interface Through {
|
|
63
|
+
component: string;
|
|
64
|
+
relation: "reads" | "read-by" | "calls" | "called-by";
|
|
65
|
+
kind?: string;
|
|
66
|
+
handled?: number;
|
|
67
|
+
unanswered?: number;
|
|
68
|
+
}
|
|
69
|
+
export interface PlacedNode extends Node {
|
|
70
|
+
through: Through[];
|
|
71
|
+
column: number;
|
|
72
|
+
row: number;
|
|
73
|
+
label: string;
|
|
74
|
+
}
|
|
75
|
+
export interface View {
|
|
76
|
+
nodes: PlacedNode[];
|
|
77
|
+
declared: Declared[];
|
|
78
|
+
calls: Call[];
|
|
79
|
+
hiddenPlatform: number;
|
|
80
|
+
}
|
|
81
|
+
export interface Options {
|
|
82
|
+
showPlatform?: boolean;
|
|
83
|
+
kinds?: string[] | null;
|
|
84
|
+
focus?: string | null;
|
|
85
|
+
}
|
|
86
|
+
/** The handled and the unanswered calls, each added up: two numbers, never one. */
|
|
87
|
+
export declare function totals(pairs: Pair[] | undefined): {
|
|
88
|
+
handled: number;
|
|
89
|
+
unanswered: number;
|
|
90
|
+
};
|
|
91
|
+
/** A failure or a call nothing answered marks a call; refusals alone never do. */
|
|
92
|
+
export declare function edgeMark(pairs: Pair[] | undefined): "warning" | null;
|
|
93
|
+
/** From 1 to 5: one step for each power of ten of the handled calls. */
|
|
94
|
+
export declare function weight(pairs: Pair[] | undefined): number;
|
|
95
|
+
/** The sentence wherever observed calls are shown: how far back they reach. */
|
|
96
|
+
export declare function observedLine(window: Window | undefined, now: number, timeZone?: string): string;
|
|
97
|
+
/** What a node is called on the page. */
|
|
98
|
+
export declare function label(node: Pick<Node, "id" | "kind">): string;
|
|
99
|
+
/**
|
|
100
|
+
* Which local service a node opens. In the installation's console no service is local, so `services`
|
|
101
|
+
* is empty and an external service is never a link; the rule is shared so the fixtures hold both.
|
|
102
|
+
*/
|
|
103
|
+
export declare function linkFor(node: Pick<Node, "id" | "kind">, services: string[]): {
|
|
104
|
+
service: string | null;
|
|
105
|
+
note: string | null;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* What to draw, given what the reader asked to see: platform folding first, then kinds, then focus,
|
|
109
|
+
* so focusing never brings back what the reader left out.
|
|
110
|
+
*/
|
|
111
|
+
export declare function view(doc: Document, options?: Options): View;
|
|
112
|
+
/** What a picture says, for a reader who cannot see it. */
|
|
113
|
+
export declare function describe(shown: View): string;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rules for reading a topology: what is shown, what is left out, and where each thing goes.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions, document in and plain data out. The local console applies the same rules
|
|
5
|
+
* (`cli/src/main/resources/console/topology.js`), and both are tested against every fixture in
|
|
6
|
+
* `console/package/fixtures/topology/`, so the two consoles cannot disagree about a rule without one
|
|
7
|
+
* suite failing. Browser-safe: nothing here imports a Node module or the server entry point.
|
|
8
|
+
*/
|
|
9
|
+
/** The handled and the unanswered calls, each added up: two numbers, never one. */
|
|
10
|
+
export function totals(pairs) {
|
|
11
|
+
const sum = { handled: 0, unanswered: 0 };
|
|
12
|
+
for (const pair of pairs ?? []) {
|
|
13
|
+
const h = pair.handled ?? {};
|
|
14
|
+
const u = pair.unanswered ?? {};
|
|
15
|
+
sum.handled += (h.ok ?? 0) + (h.refused ?? 0) + (h.failed ?? 0);
|
|
16
|
+
sum.unanswered += (u.timedOut ?? 0) + (u.undelivered ?? 0);
|
|
17
|
+
}
|
|
18
|
+
return sum;
|
|
19
|
+
}
|
|
20
|
+
/** A failure or a call nothing answered marks a call; refusals alone never do. */
|
|
21
|
+
export function edgeMark(pairs) {
|
|
22
|
+
for (const pair of pairs ?? []) {
|
|
23
|
+
const h = pair.handled ?? {};
|
|
24
|
+
const u = pair.unanswered ?? {};
|
|
25
|
+
if ((h.failed ?? 0) > 0)
|
|
26
|
+
return "warning";
|
|
27
|
+
if ((u.timedOut ?? 0) + (u.undelivered ?? 0) > 0)
|
|
28
|
+
return "warning";
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
/** From 1 to 5: one step for each power of ten of the handled calls. */
|
|
33
|
+
export function weight(pairs) {
|
|
34
|
+
const handled = totals(pairs).handled;
|
|
35
|
+
return 1 + Math.min(4, Math.floor(Math.log10(Math.max(1, handled))));
|
|
36
|
+
}
|
|
37
|
+
/** The sentence wherever observed calls are shown: how far back they reach. */
|
|
38
|
+
export function observedLine(window, now, timeZone) {
|
|
39
|
+
const seconds = window?.seconds ?? 0;
|
|
40
|
+
const since = window ? Date.parse(window.since) : NaN;
|
|
41
|
+
const sinceStart = Number.isFinite(since) && since > now - seconds * 1000;
|
|
42
|
+
const from = sinceStart ? ` (since ${clock(since, timeZone)} when the service started)` : "";
|
|
43
|
+
return `Calls observed in the last ${inWords(seconds)}${from}. Calls not made in that time are not shown.`;
|
|
44
|
+
}
|
|
45
|
+
function inWords(seconds) {
|
|
46
|
+
const count = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
47
|
+
if (seconds >= 3600 && seconds % 3600 === 0)
|
|
48
|
+
return count(seconds / 3600, "hour", "hours");
|
|
49
|
+
if (seconds >= 60 && seconds % 60 === 0)
|
|
50
|
+
return count(seconds / 60, "minute", "minutes");
|
|
51
|
+
return count(seconds, "second", "seconds");
|
|
52
|
+
}
|
|
53
|
+
function clock(millis, timeZone) {
|
|
54
|
+
return new Intl.DateTimeFormat("en-GB", { hour: "2-digit", minute: "2-digit", hour12: false, timeZone }).format(new Date(millis));
|
|
55
|
+
}
|
|
56
|
+
/** What a node is called on the page. */
|
|
57
|
+
export function label(node) {
|
|
58
|
+
if (node.kind === "UnknownCaller")
|
|
59
|
+
return "unknown caller";
|
|
60
|
+
if (node.id === "service:(other)")
|
|
61
|
+
return "other services";
|
|
62
|
+
if (node.kind === "ExternalService")
|
|
63
|
+
return node.id.slice("service:".length).split("/").pop() ?? node.id;
|
|
64
|
+
if (node.kind === "Topic")
|
|
65
|
+
return node.id.slice("topic:".length);
|
|
66
|
+
if (node.kind === "ExternalComponent")
|
|
67
|
+
return node.id.slice("external:".length);
|
|
68
|
+
if (node.kind === "Endpoint")
|
|
69
|
+
return node.id.slice("endpoint:".length);
|
|
70
|
+
return node.id;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Which local service a node opens. In the installation's console no service is local, so `services`
|
|
74
|
+
* is empty and an external service is never a link; the rule is shared so the fixtures hold both.
|
|
75
|
+
*/
|
|
76
|
+
export function linkFor(node, services) {
|
|
77
|
+
if (node.kind !== "ExternalService")
|
|
78
|
+
return { service: null, note: null };
|
|
79
|
+
if (node.id === "service:(other)")
|
|
80
|
+
return { service: null, note: "other services" };
|
|
81
|
+
const name = label(node);
|
|
82
|
+
const matches = services.filter((s) => s === name);
|
|
83
|
+
return matches.length === 1 ? { service: name, note: null } : { service: null, note: "not running here" };
|
|
84
|
+
}
|
|
85
|
+
function compare(a, b) {
|
|
86
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
87
|
+
}
|
|
88
|
+
function edgeOrder(a, b) {
|
|
89
|
+
return compare(a.from, b.from) || compare(a.to, b.to) || compare(a.kind ?? "", b.kind ?? "");
|
|
90
|
+
}
|
|
91
|
+
function throughOrder(a, b) {
|
|
92
|
+
return compare(a.component, b.component) || compare(a.relation, b.relation);
|
|
93
|
+
}
|
|
94
|
+
function fold(nodes, declared, calls) {
|
|
95
|
+
const hidden = new Set(nodes.filter((n) => n.platform).map((n) => n.id));
|
|
96
|
+
const through = new Map();
|
|
97
|
+
const note = (visible, entry) => {
|
|
98
|
+
if (!through.has(visible))
|
|
99
|
+
through.set(visible, []);
|
|
100
|
+
through.get(visible).push(entry);
|
|
101
|
+
};
|
|
102
|
+
for (const edge of declared) {
|
|
103
|
+
if (hidden.has(edge.from) && !hidden.has(edge.to))
|
|
104
|
+
note(edge.to, { component: edge.from, relation: "reads", kind: edge.kind });
|
|
105
|
+
else if (!hidden.has(edge.from) && hidden.has(edge.to))
|
|
106
|
+
note(edge.from, { component: edge.to, relation: "read-by", kind: edge.kind });
|
|
107
|
+
}
|
|
108
|
+
for (const call of calls) {
|
|
109
|
+
const counts = totals(call.pairs);
|
|
110
|
+
if (!hidden.has(call.from) && hidden.has(call.to))
|
|
111
|
+
note(call.from, { component: call.to, relation: "calls", ...counts });
|
|
112
|
+
else if (hidden.has(call.from) && !hidden.has(call.to))
|
|
113
|
+
note(call.to, { component: call.from, relation: "called-by", ...counts });
|
|
114
|
+
}
|
|
115
|
+
const visible = (e) => !hidden.has(e.from) && !hidden.has(e.to);
|
|
116
|
+
return {
|
|
117
|
+
nodes: nodes.filter((n) => !hidden.has(n.id)).map((n) => ({ ...n, through: [...(through.get(n.id) ?? [])].sort(throughOrder) })),
|
|
118
|
+
declared: declared.filter(visible),
|
|
119
|
+
calls: calls.filter(visible),
|
|
120
|
+
hidden: hidden.size,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function within(nodes, declared, calls) {
|
|
124
|
+
const ids = new Set(nodes.map((n) => n.id));
|
|
125
|
+
const both = (e) => ids.has(e.from) && ids.has(e.to);
|
|
126
|
+
return { nodes, declared: declared.filter(both), calls: calls.filter(both) };
|
|
127
|
+
}
|
|
128
|
+
function place(nodes) {
|
|
129
|
+
const layers = [...new Set(nodes.map((n) => n.layer))].sort((a, b) => a - b);
|
|
130
|
+
const rows = new Map();
|
|
131
|
+
return [...nodes]
|
|
132
|
+
.sort((a, b) => a.layer - b.layer || compare(a.id, b.id))
|
|
133
|
+
.map((node) => {
|
|
134
|
+
const column = layers.indexOf(node.layer);
|
|
135
|
+
const row = rows.get(column) ?? 0;
|
|
136
|
+
rows.set(column, row + 1);
|
|
137
|
+
return { ...node, column, row };
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* What to draw, given what the reader asked to see: platform folding first, then kinds, then focus,
|
|
142
|
+
* so focusing never brings back what the reader left out.
|
|
143
|
+
*/
|
|
144
|
+
export function view(doc, options) {
|
|
145
|
+
const asked = { showPlatform: false, kinds: null, focus: null, ...(options ?? {}) };
|
|
146
|
+
const all = (doc.nodes ?? []).map((n) => ({ ...n, through: [] }));
|
|
147
|
+
const folded = asked.showPlatform
|
|
148
|
+
? { nodes: all, declared: doc.declared ?? [], calls: doc.calls ?? [], hidden: 0 }
|
|
149
|
+
: fold(all, doc.declared ?? [], doc.calls ?? []);
|
|
150
|
+
const kinds = asked.kinds ? within(folded.nodes.filter((n) => asked.kinds.includes(n.kind)), folded.declared, folded.calls) : folded;
|
|
151
|
+
let focused = kinds;
|
|
152
|
+
if (asked.focus && kinds.nodes.some((n) => n.id === asked.focus)) {
|
|
153
|
+
const near = new Set([asked.focus]);
|
|
154
|
+
for (const e of [...kinds.declared, ...kinds.calls]) {
|
|
155
|
+
if (e.from === asked.focus)
|
|
156
|
+
near.add(e.to);
|
|
157
|
+
if (e.to === asked.focus)
|
|
158
|
+
near.add(e.from);
|
|
159
|
+
}
|
|
160
|
+
focused = within(kinds.nodes.filter((n) => near.has(n.id)), kinds.declared, kinds.calls);
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
nodes: place(focused.nodes).map((n) => ({ ...n, label: label(n) })),
|
|
164
|
+
declared: [...focused.declared].sort(edgeOrder),
|
|
165
|
+
calls: [...focused.calls].sort(edgeOrder),
|
|
166
|
+
hiddenPlatform: folded.hidden,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/** What a picture says, for a reader who cannot see it. */
|
|
170
|
+
export function describe(shown) {
|
|
171
|
+
const count = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
172
|
+
return [
|
|
173
|
+
count(shown.nodes.length, "component", "components"),
|
|
174
|
+
count(shown.declared.length, "declared connection", "declared connections"),
|
|
175
|
+
count(shown.calls.length, "observed call", "observed calls"),
|
|
176
|
+
].join(", ");
|
|
177
|
+
}
|