@fougere/admin 0.9.2-alpha.0 → 0.10.0-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/{extensions.d.ts → AdminExtension.d.ts} +5 -14
- package/dist/AdminExtension.d.ts.map +1 -0
- package/dist/{extensions.js → AdminExtension.js} +3 -3
- package/dist/AdminExtension.js.map +1 -0
- package/dist/AdminFacetRegistry.d.ts +9 -0
- package/dist/AdminFacetRegistry.d.ts.map +1 -0
- package/dist/AdminFacetRegistry.js +2 -0
- package/dist/AdminFacetRegistry.js.map +1 -0
- package/dist/AdminFacets.d.ts +6 -0
- package/dist/AdminFacets.d.ts.map +1 -0
- package/dist/{facets.js → AdminFacets.js} +1 -1
- package/dist/AdminFacets.js.map +1 -0
- package/dist/AdminFieldExtension.d.ts +7 -0
- package/dist/AdminFieldExtension.d.ts.map +1 -0
- package/dist/AdminFieldExtension.js +2 -0
- package/dist/AdminFieldExtension.js.map +1 -0
- package/dist/AdminOperation.d.ts +9 -0
- package/dist/AdminOperation.d.ts.map +1 -0
- package/dist/AdminOperation.js +2 -0
- package/dist/AdminOperation.js.map +1 -0
- package/dist/AdminOperationExtension.d.ts +6 -0
- package/dist/AdminOperationExtension.d.ts.map +1 -0
- package/dist/AdminOperationExtension.js +2 -0
- package/dist/AdminOperationExtension.js.map +1 -0
- package/dist/{resources.d.ts → AdminResource.d.ts} +11 -17
- package/dist/AdminResource.d.ts.map +1 -0
- package/dist/{resources.js → AdminResource.js} +13 -14
- package/dist/AdminResource.js.map +1 -0
- package/dist/AdminRuntime.d.ts +11 -0
- package/dist/AdminRuntime.d.ts.map +1 -0
- package/dist/{runtime.js → AdminRuntime.js} +5 -6
- package/dist/AdminRuntime.js.map +1 -0
- package/dist/AdminRuntimeOptions.d.ts +8 -0
- package/dist/AdminRuntimeOptions.d.ts.map +1 -0
- package/dist/AdminRuntimeOptions.js +2 -0
- package/dist/AdminRuntimeOptions.js.map +1 -0
- package/dist/Drawing.d.ts +39 -0
- package/dist/Drawing.d.ts.map +1 -0
- package/dist/Drawing.js +212 -0
- package/dist/Drawing.js.map +1 -0
- package/dist/EditorialFacet.d.ts +13 -0
- package/dist/EditorialFacet.d.ts.map +1 -0
- package/dist/EditorialFacet.js +2 -0
- package/dist/EditorialFacet.js.map +1 -0
- package/dist/FougereAdminProps.d.ts +17 -0
- package/dist/FougereAdminProps.d.ts.map +1 -0
- package/dist/FougereAdminProps.js +2 -0
- package/dist/FougereAdminProps.js.map +1 -0
- package/dist/FougereDashboardContextValue.d.ts +11 -0
- package/dist/FougereDashboardContextValue.d.ts.map +1 -0
- package/dist/FougereDashboardContextValue.js +2 -0
- package/dist/FougereDashboardContextValue.js.map +1 -0
- package/dist/FougereDashboardExtension.d.ts +13 -0
- package/dist/FougereDashboardExtension.d.ts.map +1 -0
- package/dist/FougereDashboardExtension.js +2 -0
- package/dist/FougereDashboardExtension.js.map +1 -0
- package/dist/FougereDashboardMetrics.d.ts +7 -0
- package/dist/FougereDashboardMetrics.d.ts.map +1 -0
- package/dist/FougereDashboardMetrics.js +2 -0
- package/dist/FougereDashboardMetrics.js.map +1 -0
- package/dist/FougereDashboardResource.d.ts +14 -0
- package/dist/FougereDashboardResource.d.ts.map +1 -0
- package/dist/FougereDashboardResource.js +2 -0
- package/dist/FougereDashboardResource.js.map +1 -0
- package/dist/FougereDashboardWidget.d.ts +11 -0
- package/dist/FougereDashboardWidget.d.ts.map +1 -0
- package/dist/FougereDashboardWidget.js +2 -0
- package/dist/FougereDashboardWidget.js.map +1 -0
- package/dist/FougereDashboardZone.d.ts +2 -0
- package/dist/FougereDashboardZone.d.ts.map +1 -0
- package/dist/FougereDashboardZone.js +2 -0
- package/dist/FougereDashboardZone.js.map +1 -0
- package/dist/{provider.d.ts → FougereDataProvider.d.ts} +2 -17
- package/dist/FougereDataProvider.d.ts.map +1 -0
- package/dist/{provider.js → FougereDataProvider.js} +2 -3
- package/dist/FougereDataProvider.js.map +1 -0
- package/dist/FougereResourceOptions.d.ts +16 -0
- package/dist/FougereResourceOptions.d.ts.map +1 -0
- package/dist/FougereResourceOptions.js +2 -0
- package/dist/FougereResourceOptions.js.map +1 -0
- package/dist/LoadedAdmin.d.ts +7 -0
- package/dist/LoadedAdmin.d.ts.map +1 -0
- package/dist/LoadedAdmin.js +2 -0
- package/dist/LoadedAdmin.js.map +1 -0
- package/dist/PlacedEdge.d.ts +17 -0
- package/dist/PlacedEdge.d.ts.map +1 -0
- package/dist/PlacedEdge.js +2 -0
- package/dist/PlacedEdge.js.map +1 -0
- package/dist/PlacedNode.d.ts +10 -0
- package/dist/PlacedNode.d.ts.map +1 -0
- package/dist/PlacedNode.js +2 -0
- package/dist/PlacedNode.js.map +1 -0
- package/dist/Point.d.ts +5 -0
- package/dist/Point.d.ts.map +1 -0
- package/dist/Point.js +2 -0
- package/dist/Point.js.map +1 -0
- package/dist/ProviderOptions.d.ts +10 -0
- package/dist/ProviderOptions.d.ts.map +1 -0
- package/dist/ProviderOptions.js +2 -0
- package/dist/ProviderOptions.js.map +1 -0
- package/dist/ReactAdminFieldContext.d.ts +10 -0
- package/dist/ReactAdminFieldContext.d.ts.map +1 -0
- package/dist/ReactAdminFieldContext.js +2 -0
- package/dist/ReactAdminFieldContext.js.map +1 -0
- package/dist/ReactAdminFieldRenderer.d.ts +4 -0
- package/dist/ReactAdminFieldRenderer.d.ts.map +1 -0
- package/dist/ReactAdminFieldRenderer.js +2 -0
- package/dist/ReactAdminFieldRenderer.js.map +1 -0
- package/dist/ReactAdminInputContext.d.ts +9 -0
- package/dist/ReactAdminInputContext.d.ts.map +1 -0
- package/dist/ReactAdminInputContext.js +2 -0
- package/dist/ReactAdminInputContext.js.map +1 -0
- package/dist/ReactAdminInputRenderer.d.ts +4 -0
- package/dist/ReactAdminInputRenderer.d.ts.map +1 -0
- package/dist/ReactAdminInputRenderer.js +2 -0
- package/dist/ReactAdminInputRenderer.js.map +1 -0
- package/dist/ReactAdminRenderers.d.ts +8 -0
- package/dist/ReactAdminRenderers.d.ts.map +1 -0
- package/dist/ReactAdminRenderers.js +2 -0
- package/dist/ReactAdminRenderers.js.map +1 -0
- package/dist/ReactAdminResourceComponents.d.ts +9 -0
- package/dist/ReactAdminResourceComponents.d.ts.map +1 -0
- package/dist/ReactAdminResourceComponents.js +2 -0
- package/dist/ReactAdminResourceComponents.js.map +1 -0
- package/dist/ResourceKey.d.ts +8 -0
- package/dist/ResourceKey.d.ts.map +1 -0
- package/dist/ResourceKey.js +2 -0
- package/dist/ResourceKey.js.map +1 -0
- package/dist/ResourceRenderOptions.d.ts +7 -0
- package/dist/ResourceRenderOptions.d.ts.map +1 -0
- package/dist/ResourceRenderOptions.js +2 -0
- package/dist/ResourceRenderOptions.js.map +1 -0
- package/dist/TopologyNode.d.ts +18 -0
- package/dist/TopologyNode.d.ts.map +1 -0
- package/dist/TopologyNode.js +2 -0
- package/dist/TopologyNode.js.map +1 -0
- package/dist/UsersFacet.d.ts +13 -0
- package/dist/UsersFacet.d.ts.map +1 -0
- package/dist/UsersFacet.js +2 -0
- package/dist/UsersFacet.js.map +1 -0
- package/dist/dashboard.d.ts +4 -61
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/dashboard.js +9 -9
- package/dist/dashboard.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/react.d.ts +21 -65
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +7 -7
- package/dist/react.js.map +1 -1
- package/dist/theme.js +1 -1
- 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/package.json +7 -5
- package/src/{extensions.ts → AdminExtension.ts} +5 -16
- package/src/AdminFacetRegistry.ts +9 -0
- package/src/AdminFacets.ts +22 -0
- package/src/AdminFieldExtension.ts +6 -0
- package/src/AdminOperation.ts +9 -0
- package/src/AdminOperationExtension.ts +5 -0
- package/src/{resources.ts → AdminResource.ts} +18 -26
- package/src/{runtime.ts → AdminRuntime.ts} +6 -24
- package/src/AdminRuntimeOptions.ts +8 -0
- package/src/Drawing.ts +261 -0
- package/src/EditorialFacet.ts +12 -0
- package/src/FougereAdminProps.ts +17 -0
- package/src/FougereDashboardContextValue.ts +11 -0
- package/src/FougereDashboardExtension.ts +13 -0
- package/src/FougereDashboardMetrics.ts +6 -0
- package/src/FougereDashboardResource.ts +14 -0
- package/src/FougereDashboardWidget.ts +11 -0
- package/src/FougereDashboardZone.ts +1 -0
- package/src/{provider.ts → FougereDataProvider.ts} +3 -26
- package/src/FougereResourceOptions.ts +13 -0
- package/src/LoadedAdmin.ts +7 -0
- package/src/PlacedEdge.ts +17 -0
- package/src/PlacedNode.ts +10 -0
- package/src/Point.ts +4 -0
- package/src/ProviderOptions.ts +10 -0
- package/src/ReactAdminFieldContext.ts +10 -0
- package/src/ReactAdminFieldRenderer.ts +4 -0
- package/src/ReactAdminInputContext.ts +9 -0
- package/src/ReactAdminInputRenderer.ts +4 -0
- package/src/ReactAdminRenderers.ts +8 -0
- package/src/ReactAdminResourceComponents.ts +9 -0
- package/src/ResourceKey.ts +7 -0
- package/src/ResourceRenderOptions.ts +7 -0
- package/src/TopologyNode.ts +18 -0
- package/src/UsersFacet.ts +12 -0
- package/src/dashboard.tsx +19 -88
- package/src/index.ts +3 -9
- package/src/react.tsx +37 -80
- package/src/theme.tsx +1 -1
- package/src/topology-page.tsx +202 -16
- package/template/index.html +5 -0
- package/template/package.json +22 -0
- package/template/src/main.tsx +27 -0
- package/template/tsconfig.json +12 -0
- package/template/vite.config.ts +16 -0
- package/dist/extensions.d.ts.map +0 -1
- package/dist/extensions.js.map +0 -1
- package/dist/facets.d.ts +0 -36
- package/dist/facets.d.ts.map +0 -1
- package/dist/facets.js.map +0 -1
- package/dist/provider.d.ts.map +0 -1
- package/dist/provider.js.map +0 -1
- package/dist/resources.d.ts.map +0 -1
- package/dist/resources.js.map +0 -1
- package/dist/runtime.d.ts +0 -22
- package/dist/runtime.d.ts.map +0 -1
- package/dist/runtime.js.map +0 -1
- package/dist/topology.d.ts +0 -18
- package/dist/topology.d.ts.map +0 -1
- package/dist/topology.js +0 -30
- package/dist/topology.js.map +0 -1
- package/src/facets.ts +0 -55
- package/src/topology.ts +0 -59
package/src/Drawing.ts
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import dagre from '@dagrejs/dagre';
|
|
2
|
+
import {
|
|
3
|
+
CALL_ENDPOINT,
|
|
4
|
+
fetcher as browserFetcher,
|
|
5
|
+
sendCall,
|
|
6
|
+
type Fetcher,
|
|
7
|
+
} from '@fougere/app/client';
|
|
8
|
+
import { ErrorCode, type FrondPlacement, type TopologyReport, Invocation } from '@fougere/core/contract';
|
|
9
|
+
import type { TopologyNode } from './TopologyNode.js';
|
|
10
|
+
import type { Point } from './Point.js';
|
|
11
|
+
import type { PlacedNode } from './PlacedNode.js';
|
|
12
|
+
import type { PlacedEdge } from './PlacedEdge.js';
|
|
13
|
+
|
|
14
|
+
/** The report, or `undefined` when the app serves no topology at all. */
|
|
15
|
+
export async function fetchTopology(
|
|
16
|
+
endpoint = CALL_ENDPOINT,
|
|
17
|
+
fetcher: Fetcher = browserFetcher,
|
|
18
|
+
): Promise<TopologyReport | undefined> {
|
|
19
|
+
try {
|
|
20
|
+
return await sendCall(
|
|
21
|
+
fetcher,
|
|
22
|
+
{ entity: 'rpc', op: 'topology' },
|
|
23
|
+
Invocation.empty,
|
|
24
|
+
endpoint,
|
|
25
|
+
) as TopologyReport;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if ((error as { code?: unknown })?.code === ErrorCode.NOT_FOUND) return undefined;
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The report read as a graph — what runs here first, then what answered, then what never did. */
|
|
33
|
+
export function nodesOf(report: TopologyReport): TopologyNode[] {
|
|
34
|
+
const order = { local: 0, remote: 1 };
|
|
35
|
+
const stated = new Map(report.declared.fronds.map((one) => [one.frond, one]));
|
|
36
|
+
const answered = new Set(report.fronds.map((frond) => frond.frond));
|
|
37
|
+
|
|
38
|
+
const around = (frond: string) => ({
|
|
39
|
+
calls: report.edges.filter((edge) => edge.from === frond),
|
|
40
|
+
calledBy: report.edges.filter((edge) => edge.to === frond),
|
|
41
|
+
declaredCalls: report.declared.edges.filter((edge) => edge.from === frond),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const heard: TopologyNode[] = [...report.fronds]
|
|
45
|
+
.sort((a, b) => order[a.placement] - order[b.placement] || a.frond.localeCompare(b.frond))
|
|
46
|
+
.map((frond) => ({
|
|
47
|
+
...frond,
|
|
48
|
+
...around(frond.frond),
|
|
49
|
+
...(stated.get(frond.frond)?.at ? { at: stated.get(frond.frond)!.at } : {}),
|
|
50
|
+
silent: false,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
// The whole reason the declared half travels: a frond that never answered is absent from
|
|
54
|
+
// the observed one, so without this the node the operator most needs to see is the one
|
|
55
|
+
// that disappears.
|
|
56
|
+
const silent: TopologyNode[] = report.declared.fronds
|
|
57
|
+
.filter((one) => one.placement === 'remote' && !answered.has(one.frond))
|
|
58
|
+
.sort((a, b) => a.frond.localeCompare(b.frond))
|
|
59
|
+
.map((one) => ({
|
|
60
|
+
frond: one.frond,
|
|
61
|
+
placement: 'remote' as const,
|
|
62
|
+
entities: 0,
|
|
63
|
+
facades: 0,
|
|
64
|
+
...around(one.frond),
|
|
65
|
+
...(one.at ? { at: one.at } : {}),
|
|
66
|
+
silent: true,
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
return [...heard, ...silent];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface Drawing {
|
|
73
|
+
nodes: PlacedNode[];
|
|
74
|
+
edges: PlacedEdge[];
|
|
75
|
+
width: number;
|
|
76
|
+
height: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const NODE = { width: 176, height: 54 };
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Where each frond sits, and the line between two of them.
|
|
83
|
+
*
|
|
84
|
+
* The placement is `@dagrejs/dagre`'s — ranks by depth, order within a rank chosen to cross as
|
|
85
|
+
* little as possible, and a polyline per edge that goes around the nodes between its ends. Two
|
|
86
|
+
* hand-written columns could not do the second or the third: a chain of three fronds put two of
|
|
87
|
+
* them in one column, and the curve between them had to bulge around whatever sat in the way.
|
|
88
|
+
*
|
|
89
|
+
* Rendering stays ours, which is why only the layout is bought: the drawing reads `error.main`
|
|
90
|
+
* and the rest of the MUI palette off the theme, and a library that painted its own canvas
|
|
91
|
+
* would have to be told the theme again on every switch. Kiali left Cytoscape for this in 2.0.0,
|
|
92
|
+
* and every service map whose source can be read — Grafana, Jaeger, Linkerd — buys the same half.
|
|
93
|
+
*/
|
|
94
|
+
export function layoutOf(nodes: readonly TopologyNode[]): Drawing {
|
|
95
|
+
const graph = new dagre.graphlib.Graph();
|
|
96
|
+
// Left to right: the reader is the process being asked, and what it depends on goes outward.
|
|
97
|
+
graph.setGraph({ rankdir: 'LR', nodesep: 26, ranksep: 92, marginx: 22, marginy: 22 });
|
|
98
|
+
graph.setDefaultEdgeLabel(() => ({}));
|
|
99
|
+
|
|
100
|
+
for (const node of nodes) graph.setNode(node.frond, { ...NODE });
|
|
101
|
+
|
|
102
|
+
const links = linksBetween(nodes);
|
|
103
|
+
for (const link of links) {
|
|
104
|
+
if (graph.hasNode(link.from) && graph.hasNode(link.to)) graph.setEdge(link.from, link.to);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
dagre.layout(graph);
|
|
108
|
+
|
|
109
|
+
const travelled = links.map((link) => link.count).filter((count): count is number => count !== undefined);
|
|
110
|
+
const shape = graph.graph();
|
|
111
|
+
|
|
112
|
+
const placed: PlacedNode[] = nodes
|
|
113
|
+
.filter((node) => graph.hasNode(node.frond))
|
|
114
|
+
.map((node) => {
|
|
115
|
+
const { x, y, width, height } = graph.node(node.frond) as PlacedNode;
|
|
116
|
+
|
|
117
|
+
return { node, x, y, width, height };
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
nodes: placed,
|
|
122
|
+
edges: links.flatMap((link) => {
|
|
123
|
+
const drawn = graph.hasNode(link.from) && graph.hasNode(link.to) ? graph.edge(link.from, link.to) : undefined;
|
|
124
|
+
if (!drawn) return [];
|
|
125
|
+
|
|
126
|
+
const points = drawn.points as Point[];
|
|
127
|
+
const at = labelAt(points, placed, figureOf(link).length * 3.4 + 8);
|
|
128
|
+
|
|
129
|
+
return [{
|
|
130
|
+
...link,
|
|
131
|
+
path: through(points),
|
|
132
|
+
...(at ? { at } : {}),
|
|
133
|
+
...(link.count === undefined ? {} : { weight: weightOf(link.count, travelled) }),
|
|
134
|
+
}];
|
|
135
|
+
}),
|
|
136
|
+
width: shape.width ?? 0,
|
|
137
|
+
height: shape.height ?? 0,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** A count as an operator reads it at a glance — `2.1k`, never `2140`. */
|
|
142
|
+
export function short(count: number): string {
|
|
143
|
+
if (count < 1000) return String(count);
|
|
144
|
+
|
|
145
|
+
return `${(count / 1000).toFixed(count < 10_000 ? 1 : 0)}k`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** What a link says of itself, and the only figure the drawing carries. */
|
|
149
|
+
export function figureOf(link: Pick<PlacedEdge, 'count' | 'errors'>): string {
|
|
150
|
+
if (link.count === undefined) return '—';
|
|
151
|
+
|
|
152
|
+
return link.errors > 0 ? `${short(link.count)} · ${short(link.errors)} ✕` : short(link.count);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Where a volume sits among the others, on a SQUARE ROOT scale. Linear makes every link but the
|
|
157
|
+
* busiest a hairline; a log flattens them the other way, and flattening is wrong at this spread —
|
|
158
|
+
* 40 to 2140 is under two orders of magnitude, and the busiest link deserves to look it.
|
|
159
|
+
*/
|
|
160
|
+
function weightOf(count: number, travelled: readonly number[]): number {
|
|
161
|
+
const high = Math.max(...travelled);
|
|
162
|
+
if (high <= 0) return 1;
|
|
163
|
+
|
|
164
|
+
return Math.sqrt(count / high);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Every link, observed or merely declared, each named once. */
|
|
168
|
+
function linksBetween(nodes: readonly TopologyNode[]): Omit<PlacedEdge, 'path'>[] {
|
|
169
|
+
const observed = nodes.flatMap((one) => one.calls);
|
|
170
|
+
const travelled = new Set(observed.map((edge) => `${edge.from} ${edge.to}`));
|
|
171
|
+
|
|
172
|
+
return [
|
|
173
|
+
...observed.map((edge) => ({ from: edge.from, to: edge.to, count: edge.count, errors: edge.errors })),
|
|
174
|
+
...nodes
|
|
175
|
+
.flatMap((one) => one.declaredCalls)
|
|
176
|
+
.filter((edge) => !travelled.has(`${edge.from} ${edge.to}`))
|
|
177
|
+
.map((edge) => ({ from: edge.from, to: edge.to, errors: 0 })),
|
|
178
|
+
];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* A B-spline over the waypoints — what dagre-d3 and Grafana draw their edges with.
|
|
183
|
+
*
|
|
184
|
+
* It deliberately does NOT pass through them. A curve pinned to the points takes its character
|
|
185
|
+
* from their COUNT, and a router hands back two for a short hop and five for one that goes around
|
|
186
|
+
* something — so every edge came out with a bearing of its own and the drawing read as noise.
|
|
187
|
+
*
|
|
188
|
+
* The ends are TRIPLED rather than joined by a straight segment: a clamped spline reaches its
|
|
189
|
+
* endpoint on its own tangent, where an anchor line meets the curve at whatever angle it happens
|
|
190
|
+
* to, which is a visible corner exactly where the arrow is.
|
|
191
|
+
*/
|
|
192
|
+
function through(points: readonly Point[]): string {
|
|
193
|
+
const first = points[0];
|
|
194
|
+
const last = points.at(-1);
|
|
195
|
+
if (!first || !last) return '';
|
|
196
|
+
if (points.length === 1) return `M ${first.x} ${first.y}`;
|
|
197
|
+
|
|
198
|
+
const held = [first, first, ...points, last, last];
|
|
199
|
+
const steps = held.slice(2).map((point, nth) => {
|
|
200
|
+
const back = held[nth]!;
|
|
201
|
+
const near = held[nth + 1]!;
|
|
202
|
+
|
|
203
|
+
return `C ${(2 * back.x + near.x) / 3} ${(2 * back.y + near.y) / 3},`
|
|
204
|
+
+ ` ${(back.x + 2 * near.x) / 3} ${(back.y + 2 * near.y) / 3},`
|
|
205
|
+
+ ` ${(back.x + 4 * near.x + point.x) / 6} ${(back.y + 4 * near.y + point.y) / 6}`;
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return `M ${first.x} ${first.y} ${steps.join(' ')}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** A point at a fraction of a route's length. */
|
|
212
|
+
function alongOf(points: readonly Point[], fraction: number): Point | undefined {
|
|
213
|
+
if (fraction <= 0 || fraction >= 1) return undefined;
|
|
214
|
+
const spans = points.slice(1).map((point, nth) => Math.hypot(point.x - points[nth]!.x, point.y - points[nth]!.y));
|
|
215
|
+
const target = spans.reduce((sum, span) => sum + span, 0) * fraction;
|
|
216
|
+
|
|
217
|
+
let walked = 0;
|
|
218
|
+
for (let nth = 0; nth < spans.length; nth++) {
|
|
219
|
+
if (walked + spans[nth]! >= target) {
|
|
220
|
+
const into = spans[nth] === 0 ? 0 : (target - walked) / spans[nth]!;
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
x: points[nth]!.x + (points[nth + 1]!.x - points[nth]!.x) * into,
|
|
224
|
+
y: points[nth]!.y + (points[nth + 1]!.y - points[nth]!.y) * into,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
walked += spans[nth]!;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return points.at(-1);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Where a link's figure sits: halfway ALONG the route, then moved along it until it sits inside no
|
|
235
|
+
* node. The middle waypoint is not the middle of the line — a routed edge bunches its points near
|
|
236
|
+
* the ends — and a figure drawn over a card cannot be read at all.
|
|
237
|
+
*/
|
|
238
|
+
function labelAt(points: readonly Point[], boxes: readonly PlacedNode[], half: number): Point | undefined {
|
|
239
|
+
const inside = (point: Point) => boxes.some((box) =>
|
|
240
|
+
Math.abs(point.x - box.x) < box.width / 2 + half && Math.abs(point.y - box.y) < box.height / 2 + 13);
|
|
241
|
+
|
|
242
|
+
const middle = alongOf(points, 0.5);
|
|
243
|
+
if (!middle || !inside(middle)) return middle;
|
|
244
|
+
|
|
245
|
+
for (let step = 1; step <= 12; step++) {
|
|
246
|
+
for (const side of [-1, 1]) {
|
|
247
|
+
const moved = alongOf(points, 0.5 + side * step * 0.035);
|
|
248
|
+
if (moved && !inside(moved)) return moved;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return middle;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* A frond this process called that publishes nothing of its own shape. Read AFTER `silent`:
|
|
257
|
+
* a frond that never answered has the same two zeroes and a different reason for them.
|
|
258
|
+
*/
|
|
259
|
+
export function isOpaque(node: FrondPlacement): boolean {
|
|
260
|
+
return node.placement === 'remote' && node.entities === 0 && node.facades === 0;
|
|
261
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface EditorialFacet {
|
|
2
|
+
/** The field that carries the human-readable title. */
|
|
3
|
+
title: string;
|
|
4
|
+
/** The lifecycle vocabulary, grouped by meaning rather than by raw enum value. */
|
|
5
|
+
state?: {
|
|
6
|
+
field: string;
|
|
7
|
+
draft?: readonly string[];
|
|
8
|
+
published?: readonly string[];
|
|
9
|
+
};
|
|
10
|
+
createdAt?: string;
|
|
11
|
+
updatedAt?: string;
|
|
12
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { AdminProps as BaseAdminProps } from 'react-admin';
|
|
2
|
+
import type { FougereDashboardExtension } from './FougereDashboardExtension.js';
|
|
3
|
+
import type { AdminExtension } from './AdminExtension.js';
|
|
4
|
+
import type { Fetcher } from '@fougere/app/client';
|
|
5
|
+
import type { ReactAdminRenderers } from './ReactAdminRenderers.js';
|
|
6
|
+
import type { ReactAdminResourceComponents } from './ReactAdminResourceComponents.js';
|
|
7
|
+
|
|
8
|
+
export type FougereAdminProps = Omit<BaseAdminProps, 'children' | 'dataProvider'> & {
|
|
9
|
+
endpoint?: string;
|
|
10
|
+
fetcher?: Fetcher;
|
|
11
|
+
extensions?: readonly AdminExtension[];
|
|
12
|
+
renderers?: ReactAdminRenderers;
|
|
13
|
+
/** Add, move, resize, replace or hide widgets without snapshotting the dashboard. */
|
|
14
|
+
dashboardExtensions?: readonly FougereDashboardExtension[];
|
|
15
|
+
/** Explicit page-level escape hatches, scoped to one resource and one view. */
|
|
16
|
+
resourceComponents?: Record<string, ReactAdminResourceComponents>;
|
|
17
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { FougereDashboardResource } from './FougereDashboardResource.js';
|
|
2
|
+
import type { FougereDashboardMetrics } from './FougereDashboardMetrics.js';
|
|
3
|
+
|
|
4
|
+
export interface FougereDashboardContextValue {
|
|
5
|
+
loading: boolean;
|
|
6
|
+
resources: FougereDashboardResource[];
|
|
7
|
+
editorial: FougereDashboardResource[];
|
|
8
|
+
users: FougereDashboardResource[];
|
|
9
|
+
metrics: FougereDashboardMetrics;
|
|
10
|
+
navigate(view: 'list' | 'show' | 'edit' | 'create', resource: string, id?: string | number): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import type { FougereDashboardZone } from './FougereDashboardZone.js';
|
|
3
|
+
|
|
4
|
+
/** A delta over stable widget ids; a component on a new id contributes a new widget. */
|
|
5
|
+
export interface FougereDashboardExtension {
|
|
6
|
+
widget: string;
|
|
7
|
+
component?: ComponentType;
|
|
8
|
+
zone?: FougereDashboardZone;
|
|
9
|
+
span?: number;
|
|
10
|
+
hidden?: boolean;
|
|
11
|
+
before?: string;
|
|
12
|
+
after?: string;
|
|
13
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AdminFacets } from './AdminFacets.js';
|
|
2
|
+
|
|
3
|
+
export interface FougereDashboardResource {
|
|
4
|
+
name: string;
|
|
5
|
+
label: string;
|
|
6
|
+
primary: string;
|
|
7
|
+
facets: AdminFacets;
|
|
8
|
+
hasCreate: boolean;
|
|
9
|
+
hasEdit: boolean;
|
|
10
|
+
hasShow: boolean;
|
|
11
|
+
total: number;
|
|
12
|
+
rows: Record<string, unknown>[];
|
|
13
|
+
states: Record<string, number>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
import type { FougereDashboardZone } from './FougereDashboardZone.js';
|
|
3
|
+
|
|
4
|
+
export interface FougereDashboardWidget {
|
|
5
|
+
id: string;
|
|
6
|
+
zone: FougereDashboardZone;
|
|
7
|
+
/** Twelve-column width for `main`, four-column width for `metrics`. */
|
|
8
|
+
span: number;
|
|
9
|
+
component: ComponentType;
|
|
10
|
+
hidden?: boolean;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type FougereDashboardZone = 'hero' | 'metrics' | 'main';
|
|
@@ -1,30 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
sendCall,
|
|
4
|
-
fetcher as browserFetcher,
|
|
5
|
-
CALL_ENDPOINT,
|
|
6
|
-
itemsOf,
|
|
7
|
-
pageOf,
|
|
8
|
-
errorsByField,
|
|
9
|
-
type Fetcher,
|
|
10
|
-
} from '@fougere/app/client';
|
|
1
|
+
import { sendCall, fetcher as browserFetcher, CALL_ENDPOINT, itemsOf, pageOf, errorsByField } from '@fougere/app/client';
|
|
11
2
|
import { validationErrorsOf } from '@fougere/core/contract';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export interface ResourceKey {
|
|
15
|
-
/** The registration key its door answers under — `post`, not `Post`. */
|
|
16
|
-
name: string;
|
|
17
|
-
/** The field that identifies an instance. */
|
|
18
|
-
primary: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export interface ProviderOptions {
|
|
22
|
-
/** Registration key → its identity. Built from the card by `resourcesOf`. */
|
|
23
|
-
resources: Record<string, ResourceKey>;
|
|
24
|
-
/** The call endpoint. A named surface appends `/{surface}` to it. */
|
|
25
|
-
endpoint?: string;
|
|
26
|
-
fetcher?: Fetcher;
|
|
27
|
-
}
|
|
3
|
+
import type { ResourceKey } from './ResourceKey.js';
|
|
4
|
+
import type { ProviderOptions } from './ProviderOptions.js';
|
|
28
5
|
|
|
29
6
|
type Values = Record<string, unknown>;
|
|
30
7
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ResourceOptions } from 'react-admin';
|
|
2
|
+
import type { AdminFacets } from './AdminFacets.js';
|
|
3
|
+
|
|
4
|
+
export interface FougereResourceOptions extends ResourceOptions {
|
|
5
|
+
primary: string;
|
|
6
|
+
facets: AdminFacets;
|
|
7
|
+
/** The frond that owns this facade — the card groups by it, so the panel can too. */
|
|
8
|
+
frond?: string;
|
|
9
|
+
/** What the facade answers, with each op's kind. `query` reads, `command` writes. */
|
|
10
|
+
operations?: readonly { name: string; kind: 'query' | 'command' }[];
|
|
11
|
+
/** How many columns the shape yields — a rough measure of an entity's width. */
|
|
12
|
+
fieldCount?: number;
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Point } from './Point.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One link between two nodes. `count` is absent on a link nothing has travelled: the config
|
|
5
|
+
* declares it and no call has gone down it, which is drawn broken rather than not drawn.
|
|
6
|
+
*/
|
|
7
|
+
export interface PlacedEdge {
|
|
8
|
+
from: string;
|
|
9
|
+
to: string;
|
|
10
|
+
path: string;
|
|
11
|
+
/** Where this link's figure goes — clear of every node, or absent when the route is a point. */
|
|
12
|
+
at?: Point;
|
|
13
|
+
count?: number;
|
|
14
|
+
errors: number;
|
|
15
|
+
/** Where this link's volume sits among the others, `0` to `1`. Absent when nothing travelled. */
|
|
16
|
+
weight?: number;
|
|
17
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TopologyNode } from './TopologyNode.js';
|
|
2
|
+
|
|
3
|
+
/** A node placed on the drawing. `x`/`y` are its CENTRE, the convention the layout answers in. */
|
|
4
|
+
export interface PlacedNode {
|
|
5
|
+
node: TopologyNode;
|
|
6
|
+
x: number;
|
|
7
|
+
y: number;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
}
|
package/src/Point.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { fetcher as browserFetcher, type Fetcher } from '@fougere/app/client';
|
|
2
|
+
import type { ResourceKey } from './ResourceKey.js';
|
|
3
|
+
|
|
4
|
+
export interface ProviderOptions {
|
|
5
|
+
/** Registration key → its identity. Built from the card by `resourcesOf`. */
|
|
6
|
+
resources: Record<string, ResourceKey>;
|
|
7
|
+
/** The call endpoint. A named surface appends `/{surface}` to it. */
|
|
8
|
+
endpoint?: string;
|
|
9
|
+
fetcher?: Fetcher;
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ReactElement } from 'react';
|
|
2
|
+
import { type AdminResource } from './AdminResource.js';
|
|
3
|
+
import { type TableColumn } from '@fougere/app/client';
|
|
4
|
+
|
|
5
|
+
export interface ReactAdminFieldContext {
|
|
6
|
+
resource: AdminResource;
|
|
7
|
+
column: TableColumn;
|
|
8
|
+
/** The maintained Fougere renderer. Call it to wrap rather than replace it. */
|
|
9
|
+
defaultRender(): ReactElement;
|
|
10
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ReactElement } from 'react';
|
|
2
|
+
import { type AdminResource } from './AdminResource.js';
|
|
3
|
+
import { type FormField } from '@fougere/app/client';
|
|
4
|
+
|
|
5
|
+
export interface ReactAdminInputContext {
|
|
6
|
+
resource: AdminResource;
|
|
7
|
+
field: FormField;
|
|
8
|
+
defaultRender(): ReactElement;
|
|
9
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReactAdminFieldRenderer } from './ReactAdminFieldRenderer.js';
|
|
2
|
+
import type { ReactAdminInputRenderer } from './ReactAdminInputRenderer.js';
|
|
3
|
+
|
|
4
|
+
export interface ReactAdminRenderers {
|
|
5
|
+
/** Exact `resource.field` keys. Unmentioned and future fields keep the default renderer. */
|
|
6
|
+
fields?: Record<string, ReactAdminFieldRenderer>;
|
|
7
|
+
inputs?: Record<string, ReactAdminInputRenderer>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** What a resource must tell the provider — the rest of the card is the UI's business. */
|
|
2
|
+
export interface ResourceKey {
|
|
3
|
+
/** The registration key its facade answers under — `post`, not `Post`. */
|
|
4
|
+
name: string;
|
|
5
|
+
/** The field that identifies an instance. */
|
|
6
|
+
primary: string;
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ReactAdminRenderers } from './ReactAdminRenderers.js';
|
|
2
|
+
import type { ReactAdminResourceComponents } from './ReactAdminResourceComponents.js';
|
|
3
|
+
|
|
4
|
+
export interface ResourceRenderOptions {
|
|
5
|
+
renderers?: ReactAdminRenderers;
|
|
6
|
+
components?: ReactAdminResourceComponents;
|
|
7
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type DeclaredEdge, type Edge, type FrondPlacement } from '@fougere/core/contract';
|
|
2
|
+
|
|
3
|
+
/** One frond as the page draws it: its placement, and the calls observed around it. */
|
|
4
|
+
export interface TopologyNode extends FrondPlacement {
|
|
5
|
+
/** Fronds this one called, with what it cost them. */
|
|
6
|
+
calls: Edge[];
|
|
7
|
+
/** Fronds that called this one. */
|
|
8
|
+
calledBy: Edge[];
|
|
9
|
+
/** Fronds its code reaches, whether or not a call has ever gone down the link. */
|
|
10
|
+
declaredCalls: DeclaredEdge[];
|
|
11
|
+
/** Where the config says to reach it, when it says so. */
|
|
12
|
+
at?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Declared in `remotes:` and never heard from. A different absence from an opaque remote,
|
|
15
|
+
* which answered and keeps its shape to itself: this one may simply be down.
|
|
16
|
+
*/
|
|
17
|
+
silent: boolean;
|
|
18
|
+
}
|