@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/dashboard.tsx
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import type { FougereResourceOptions } from './FougereResourceOptions.js';
|
|
2
|
+
import type { FougereDashboardResource } from './FougereDashboardResource.js';
|
|
3
|
+
import type { FougereDashboardMetrics } from './FougereDashboardMetrics.js';
|
|
4
|
+
import type { FougereDashboardContextValue } from './FougereDashboardContextValue.js';
|
|
5
|
+
import type { FougereDashboardZone } from './FougereDashboardZone.js';
|
|
6
|
+
import type { FougereDashboardWidget } from './FougereDashboardWidget.js';
|
|
7
|
+
import type { FougereDashboardExtension } from './FougereDashboardExtension.js';
|
|
1
8
|
'use client';
|
|
2
9
|
|
|
3
10
|
import {
|
|
@@ -12,65 +19,11 @@ import {
|
|
|
12
19
|
Typography,
|
|
13
20
|
type SvgIconProps,
|
|
14
21
|
} from '@mui/material';
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
useContext,
|
|
18
|
-
useEffect,
|
|
19
|
-
useMemo,
|
|
20
|
-
useState,
|
|
21
|
-
type ComponentType,
|
|
22
|
-
type ReactElement,
|
|
23
|
-
} from 'react';
|
|
24
|
-
import {
|
|
25
|
-
useDataProvider,
|
|
26
|
-
useLocaleState,
|
|
27
|
-
useRedirect,
|
|
28
|
-
useResourceDefinitions,
|
|
29
|
-
useTranslate,
|
|
30
|
-
Title,
|
|
31
|
-
type ResourceOptions,
|
|
32
|
-
} from 'react-admin';
|
|
33
|
-
import type { AdminFacets, EditorialFacet, UsersFacet } from './facets.js';
|
|
34
|
-
|
|
35
|
-
export interface FougereResourceOptions extends ResourceOptions {
|
|
36
|
-
primary: string;
|
|
37
|
-
facets: AdminFacets;
|
|
38
|
-
/** The frond that owns this door — the card groups by it, so the panel can too. */
|
|
39
|
-
frond?: string;
|
|
40
|
-
/** What the door answers, with each op's kind. `query` reads, `command` writes. */
|
|
41
|
-
operations?: readonly { name: string; kind: 'query' | 'command' }[];
|
|
42
|
-
/** How many columns the shape yields — a rough measure of an entity's width. */
|
|
43
|
-
fieldCount?: number;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface FougereDashboardResource {
|
|
47
|
-
name: string;
|
|
48
|
-
label: string;
|
|
49
|
-
primary: string;
|
|
50
|
-
facets: AdminFacets;
|
|
51
|
-
hasCreate: boolean;
|
|
52
|
-
hasEdit: boolean;
|
|
53
|
-
hasShow: boolean;
|
|
54
|
-
total: number;
|
|
55
|
-
rows: Record<string, unknown>[];
|
|
56
|
-
states: Record<string, number>;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface FougereDashboardMetrics {
|
|
60
|
-
content: number;
|
|
61
|
-
drafts: number;
|
|
62
|
-
published: number;
|
|
63
|
-
users: number;
|
|
64
|
-
}
|
|
22
|
+
import { createContext, useContext, useEffect, useMemo, useState, type ReactElement } from 'react';
|
|
23
|
+
import { useDataProvider, useLocaleState, useRedirect, useResourceDefinitions, useTranslate, Title } from 'react-admin';
|
|
65
24
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
resources: FougereDashboardResource[];
|
|
69
|
-
editorial: FougereDashboardResource[];
|
|
70
|
-
users: FougereDashboardResource[];
|
|
71
|
-
metrics: FougereDashboardMetrics;
|
|
72
|
-
navigate(view: 'list' | 'show' | 'edit' | 'create', resource: string, id?: string | number): void;
|
|
73
|
-
}
|
|
25
|
+
import type { EditorialFacet } from './EditorialFacet.js';
|
|
26
|
+
import type { UsersFacet } from './UsersFacet.js';
|
|
74
27
|
|
|
75
28
|
const DashboardContext = createContext<FougereDashboardContextValue | undefined>(undefined);
|
|
76
29
|
|
|
@@ -81,28 +34,6 @@ export function useFougereDashboard(): FougereDashboardContextValue {
|
|
|
81
34
|
return context;
|
|
82
35
|
}
|
|
83
36
|
|
|
84
|
-
export type FougereDashboardZone = 'hero' | 'metrics' | 'main';
|
|
85
|
-
|
|
86
|
-
export interface FougereDashboardWidget {
|
|
87
|
-
id: string;
|
|
88
|
-
zone: FougereDashboardZone;
|
|
89
|
-
/** Twelve-column width for `main`, four-column width for `metrics`. */
|
|
90
|
-
span: number;
|
|
91
|
-
component: ComponentType;
|
|
92
|
-
hidden?: boolean;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** A delta over stable widget ids; a component on a new id contributes a new widget. */
|
|
96
|
-
export interface FougereDashboardExtension {
|
|
97
|
-
widget: string;
|
|
98
|
-
component?: ComponentType;
|
|
99
|
-
zone?: FougereDashboardZone;
|
|
100
|
-
span?: number;
|
|
101
|
-
hidden?: boolean;
|
|
102
|
-
before?: string;
|
|
103
|
-
after?: string;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
37
|
export function applyDashboardExtensions(
|
|
107
38
|
defaults: readonly FougereDashboardWidget[],
|
|
108
39
|
extensions: readonly FougereDashboardExtension[] = [],
|
|
@@ -388,7 +319,7 @@ function UsersWidget(): ReactElement {
|
|
|
388
319
|
* What the application IS, as opposed to what it holds.
|
|
389
320
|
*
|
|
390
321
|
* Every other widget counts rows; this one counts the shape that produced them —
|
|
391
|
-
* fronds,
|
|
322
|
+
* fronds, facades, operations split by kind, and how wide each entity is. All of it comes
|
|
392
323
|
* from the same card the menu was built from, so it costs no query at all.
|
|
393
324
|
*
|
|
394
325
|
* **Remotes are not in it, and the card is why.** `identityCardOf` maps `app.fronds`,
|
|
@@ -402,7 +333,7 @@ function StructureWidget(): ReactElement {
|
|
|
402
333
|
const definitions = useResourceDefinitions();
|
|
403
334
|
|
|
404
335
|
const structure = useMemo(() => {
|
|
405
|
-
const byFrond = new Map<string, {
|
|
336
|
+
const byFrond = new Map<string, { facades: number; queries: number; commands: number; fields: number }>();
|
|
406
337
|
let queries = 0;
|
|
407
338
|
let commands = 0;
|
|
408
339
|
let fields = 0;
|
|
@@ -414,9 +345,9 @@ function StructureWidget(): ReactElement {
|
|
|
414
345
|
const c = ops.length - q;
|
|
415
346
|
const width = options?.fieldCount ?? 0;
|
|
416
347
|
queries += q; commands += c; fields += width;
|
|
417
|
-
const held = byFrond.get(frond) ?? {
|
|
348
|
+
const held = byFrond.get(frond) ?? { facades: 0, queries: 0, commands: 0, fields: 0 };
|
|
418
349
|
byFrond.set(frond, {
|
|
419
|
-
|
|
350
|
+
facades: held.facades + 1,
|
|
420
351
|
queries: held.queries + q,
|
|
421
352
|
commands: held.commands + c,
|
|
422
353
|
fields: held.fields + width,
|
|
@@ -424,14 +355,14 @@ function StructureWidget(): ReactElement {
|
|
|
424
355
|
}
|
|
425
356
|
return {
|
|
426
357
|
fronds: [...byFrond.entries()].map(([name, counts]) => ({ name, ...counts })),
|
|
427
|
-
|
|
358
|
+
facades: Object.keys(definitions).length,
|
|
428
359
|
queries, commands, fields,
|
|
429
360
|
};
|
|
430
361
|
}, [definitions]);
|
|
431
362
|
|
|
432
363
|
const totals: [string, string, number][] = [
|
|
433
364
|
['structure.fronds', 'Fronds', structure.fronds.length],
|
|
434
|
-
['structure.
|
|
365
|
+
['structure.facades', 'Facades', structure.facades],
|
|
435
366
|
['structure.queries', 'Queries', structure.queries],
|
|
436
367
|
['structure.commands', 'Commands', structure.commands],
|
|
437
368
|
];
|
|
@@ -468,7 +399,7 @@ function StructureWidget(): ReactElement {
|
|
|
468
399
|
<Box sx={{ minWidth: 0 }}>
|
|
469
400
|
<Typography noWrap sx={{ fontWeight: 640 }}>{frond.name}</Typography>
|
|
470
401
|
<Typography variant="caption" color="text.secondary">
|
|
471
|
-
{t('structure.frondDoors', `${frond.
|
|
402
|
+
{t('structure.frondDoors', `${frond.facades} facades`, { smart_count: frond.facades })}
|
|
472
403
|
{' · '}
|
|
473
404
|
{t('structure.frondFields', `${frond.fields} fields`, { smart_count: frond.fields })}
|
|
474
405
|
</Typography>
|
|
@@ -495,7 +426,7 @@ function CollectionsWidget(): ReactElement {
|
|
|
495
426
|
const { loading, resources, navigate } = useFougereDashboard();
|
|
496
427
|
return (
|
|
497
428
|
<Card><CardContent sx={{ p: 2.5, '&:last-child': { pb: 2.5 } }}>
|
|
498
|
-
<Typography variant="h5">{t('collections.title', 'Collections')}</Typography><Typography variant="body2" color="text.secondary" sx={{ mt: .4, mb: 2 }}>{t('collections.subtitle', 'Every
|
|
429
|
+
<Typography variant="h5">{t('collections.title', 'Collections')}</Typography><Typography variant="body2" color="text.secondary" sx={{ mt: .4, mb: 2 }}>{t('collections.subtitle', 'Every facade the card announced')}</Typography>
|
|
499
430
|
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', sm: 'repeat(2,minmax(0,1fr))', lg: 'repeat(3,minmax(0,1fr))' }, gap: 1 }}>
|
|
500
431
|
{loading ? [1, 2, 3].map((key) => <Skeleton key={key} height={58} />) : resources.map((resource) => (
|
|
501
432
|
<Box key={resource.name} component="button" onClick={() => navigate('list', resource.name)} sx={{ appearance: 'none', width: '100%', display: 'flex', alignItems: 'center', gap: 1.5, p: 1.25, color: 'text.primary', bgcolor: 'transparent', border: 0, borderRadius: 2, textAlign: 'left', cursor: 'pointer', font: 'inherit', '&:hover': { bgcolor: 'action.hover' } }}>
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
export {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from './extensions.js';
|
|
5
|
-
export {
|
|
6
|
-
type TopologyReport,
|
|
7
|
-
type FrondPlacement,
|
|
8
|
-
type Edge,
|
|
9
|
-
} from './topology.js';
|
|
1
|
+
export { defineAdminExtension, type AdminExtension } from './AdminExtension.js';
|
|
2
|
+
export { type FrondPlacement } from '@fougere/core';
|
|
3
|
+
export { type Edge, type TopologyReport } from '@fougere/core';
|
package/src/react.tsx
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
export type { ReactAdminFieldContext } from './ReactAdminFieldContext.js';
|
|
2
|
+
export type { ReactAdminInputContext } from './ReactAdminInputContext.js';
|
|
3
|
+
export type { ReactAdminFieldRenderer } from './ReactAdminFieldRenderer.js';
|
|
4
|
+
export type { ReactAdminInputRenderer } from './ReactAdminInputRenderer.js';
|
|
5
|
+
export type { ReactAdminRenderers } from './ReactAdminRenderers.js';
|
|
6
|
+
export type { ReactAdminResourceComponents } from './ReactAdminResourceComponents.js';
|
|
7
|
+
export type { ResourceRenderOptions } from './ResourceRenderOptions.js';
|
|
8
|
+
export type { FougereAdminProps } from './FougereAdminProps.js';
|
|
9
|
+
import type { ReactAdminRenderers } from './ReactAdminRenderers.js';
|
|
10
|
+
|
|
11
|
+
import type { ResourceRenderOptions } from './ResourceRenderOptions.js';
|
|
12
|
+
import type { FougereAdminProps } from './FougereAdminProps.js';
|
|
1
13
|
'use client';
|
|
2
14
|
/**
|
|
3
15
|
* The rendering half — a column becomes a field, a form field becomes an input.
|
|
@@ -16,24 +28,22 @@ import {
|
|
|
16
28
|
} from 'react-admin';
|
|
17
29
|
import { Route } from 'react-router-dom';
|
|
18
30
|
import { Box, Button, Card, CardContent, Dialog, DialogActions, DialogContent, DialogTitle, TextField as MuiTextField, Typography } from '@mui/material';
|
|
19
|
-
import { cloneElement, useMemo, useState, type ComponentProps, type
|
|
20
|
-
import { createAdminRuntime } from './
|
|
21
|
-
import type { AdminExtension } from './
|
|
22
|
-
import {
|
|
23
|
-
import
|
|
24
|
-
import type {
|
|
31
|
+
import { cloneElement, useMemo, useState, type ComponentProps, type ReactElement } from 'react';
|
|
32
|
+
import { createAdminRuntime } from './AdminRuntime.js';
|
|
33
|
+
import type { AdminExtension } from './AdminExtension.js';
|
|
34
|
+
import { type AdminOperation } from './AdminOperation.js';
|
|
35
|
+
import { actionsOf, type AdminResource } from './AdminResource.js';
|
|
36
|
+
import type { EditorialFacet } from './EditorialFacet.js';
|
|
37
|
+
import type { UsersFacet } from './UsersFacet.js';
|
|
38
|
+
|
|
25
39
|
import { formFieldsOf, type FormField, type TableColumn } from '@fougere/app/client';
|
|
26
40
|
import { Card as SchemaCard } from '@fougere/schema';
|
|
27
41
|
import { FougereLayout, fougereDarkTheme, fougereLightTheme } from './theme.js';
|
|
28
42
|
import { FougereTopology } from './topology-page.js';
|
|
29
43
|
export { FougereTopology, type FougereTopologyProps } from './topology-page.js';
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
FougereUsersIcon,
|
|
34
|
-
type FougereDashboardExtension,
|
|
35
|
-
type FougereResourceOptions,
|
|
36
|
-
} from './dashboard.js';
|
|
44
|
+
|
|
45
|
+
import { type FougereResourceOptions } from './FougereResourceOptions.js';
|
|
46
|
+
import { FougereContentIcon, FougereDashboard, FougereUsersIcon } from './dashboard.js';
|
|
37
47
|
|
|
38
48
|
export {
|
|
39
49
|
FougereAppBar,
|
|
@@ -42,21 +52,14 @@ export {
|
|
|
42
52
|
fougereDarkTheme,
|
|
43
53
|
fougereLightTheme,
|
|
44
54
|
} from './theme.js';
|
|
45
|
-
export {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
type FougereDashboardExtension,
|
|
54
|
-
type FougereDashboardMetrics,
|
|
55
|
-
type FougereDashboardResource,
|
|
56
|
-
type FougereDashboardWidget,
|
|
57
|
-
type FougereDashboardZone,
|
|
58
|
-
type FougereResourceOptions,
|
|
59
|
-
} from './dashboard.js';
|
|
55
|
+
export { type FougereDashboardContextValue } from './FougereDashboardContextValue.js';
|
|
56
|
+
export { type FougereDashboardExtension } from './FougereDashboardExtension.js';
|
|
57
|
+
export { type FougereDashboardMetrics } from './FougereDashboardMetrics.js';
|
|
58
|
+
export { type FougereDashboardResource } from './FougereDashboardResource.js';
|
|
59
|
+
export { type FougereDashboardWidget } from './FougereDashboardWidget.js';
|
|
60
|
+
export { type FougereDashboardZone } from './FougereDashboardZone.js';
|
|
61
|
+
export { type FougereResourceOptions } from './FougereResourceOptions.js';
|
|
62
|
+
export { FOUGERE_DASHBOARD_WIDGETS, FougereContentIcon, FougereDashboard, FougereUsersIcon, applyDashboardExtensions, useFougereDashboard } from './dashboard.js';
|
|
60
63
|
|
|
61
64
|
const FIELDS = {
|
|
62
65
|
text: TextField, number: NumberField, boolean: BooleanField, date: DateField,
|
|
@@ -143,41 +146,6 @@ function defaultInputFor(field: FormField, t: Translate): ReactElement {
|
|
|
143
146
|
);
|
|
144
147
|
}
|
|
145
148
|
|
|
146
|
-
export interface ReactAdminFieldContext {
|
|
147
|
-
resource: AdminResource;
|
|
148
|
-
column: TableColumn;
|
|
149
|
-
/** The maintained Fougere renderer. Call it to wrap rather than replace it. */
|
|
150
|
-
defaultRender(): ReactElement;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export interface ReactAdminInputContext {
|
|
154
|
-
resource: AdminResource;
|
|
155
|
-
field: FormField;
|
|
156
|
-
defaultRender(): ReactElement;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export type ReactAdminFieldRenderer = (context: ReactAdminFieldContext) => ReactElement;
|
|
160
|
-
export type ReactAdminInputRenderer = (context: ReactAdminInputContext) => ReactElement;
|
|
161
|
-
|
|
162
|
-
export interface ReactAdminRenderers {
|
|
163
|
-
/** Exact `resource.field` keys. Unmentioned and future fields keep the default renderer. */
|
|
164
|
-
fields?: Record<string, ReactAdminFieldRenderer>;
|
|
165
|
-
inputs?: Record<string, ReactAdminInputRenderer>;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export interface ReactAdminResourceComponents {
|
|
169
|
-
list?: ComponentType;
|
|
170
|
-
show?: ComponentType;
|
|
171
|
-
edit?: ComponentType;
|
|
172
|
-
create?: ComponentType;
|
|
173
|
-
icon?: ComponentType;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export interface ResourceRenderOptions {
|
|
177
|
-
renderers?: ReactAdminRenderers;
|
|
178
|
-
components?: ReactAdminResourceComponents;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
149
|
function fieldFor(resource: AdminResource, column: TableColumn, t: Translate, renderers?: ReactAdminRenderers): ReactElement {
|
|
182
150
|
const defaultRender = () => defaultFieldFor(column, t);
|
|
183
151
|
const renderer = renderers?.fields?.[`${resource.name}.${column.name}`];
|
|
@@ -202,7 +170,7 @@ function inputFor(resource: AdminResource, field: FormField, t: Translate, rende
|
|
|
202
170
|
* an article from the back-office.
|
|
203
171
|
*
|
|
204
172
|
* Three things are derived and none is declared per entity: WHICH ops (everything the
|
|
205
|
-
*
|
|
173
|
+
* facade serves beyond the five verbs), WHETHER it asks for input (`op.input` — then the
|
|
206
174
|
* dialog's form is `formFieldsOf` over the reconstructed schema, the same projection an
|
|
207
175
|
* ordinary form uses), and WHETHER it confirms (`op.confirm`, from an extension).
|
|
208
176
|
*/
|
|
@@ -286,7 +254,7 @@ function OperationButton({
|
|
|
286
254
|
);
|
|
287
255
|
}
|
|
288
256
|
|
|
289
|
-
/** Row-level actions: the verbs the
|
|
257
|
+
/** Row-level actions: the verbs the facade serves, then everything else it serves. */
|
|
290
258
|
function RowActions({ resource }: { resource: AdminResource }): ReactElement {
|
|
291
259
|
const actions = useMemo(() => actionsOf(resource.operations), [resource]);
|
|
292
260
|
return (
|
|
@@ -304,9 +272,9 @@ const listFor = (r: AdminResource, renderers?: ReactAdminRenderers) => function
|
|
|
304
272
|
const t = useTranslate();
|
|
305
273
|
return (
|
|
306
274
|
<List>
|
|
307
|
-
{/* `can.delete` had no reader, so a
|
|
275
|
+
{/* `can.delete` had no reader, so a facade serving no `delete` still showed the
|
|
308
276
|
selection checkboxes and the bulk Delete button — every one of them a 404. And
|
|
309
|
-
a
|
|
277
|
+
a facade serving `update` but not `findById` had an inert row. */}
|
|
310
278
|
<Datagrid
|
|
311
279
|
rowClick={r.can.show ? 'show' : r.can.edit ? 'edit' : false}
|
|
312
280
|
bulkActionButtons={r.can.delete ? undefined : false}
|
|
@@ -353,7 +321,7 @@ const createFor = (r: AdminResource, renderers?: ReactAdminRenderers) => functio
|
|
|
353
321
|
return <Create><SimpleForm>{r.fields.map((field) => inputFor(r, field, t, renderers))}</SimpleForm></Create>;
|
|
354
322
|
};
|
|
355
323
|
|
|
356
|
-
/** One
|
|
324
|
+
/** One facade, as the four pages the card says it serves. */
|
|
357
325
|
export function resourceFor(r: AdminResource, options: ResourceRenderOptions = {}): ReactElement {
|
|
358
326
|
const { renderers, components = {} } = options;
|
|
359
327
|
const resourceOptions: FougereResourceOptions = {
|
|
@@ -403,17 +371,6 @@ export function resourceFor(r: AdminResource, options: ResourceRenderOptions = {
|
|
|
403
371
|
*/
|
|
404
372
|
type BaseAdminProps = ComponentProps<typeof Admin>;
|
|
405
373
|
|
|
406
|
-
export type FougereAdminProps = Omit<BaseAdminProps, 'children' | 'dataProvider'> & {
|
|
407
|
-
endpoint?: string;
|
|
408
|
-
fetcher?: Fetcher;
|
|
409
|
-
extensions?: readonly AdminExtension[];
|
|
410
|
-
renderers?: ReactAdminRenderers;
|
|
411
|
-
/** Add, move, resize, replace or hide widgets without snapshotting the dashboard. */
|
|
412
|
-
dashboardExtensions?: readonly FougereDashboardExtension[];
|
|
413
|
-
/** Explicit page-level escape hatches, scoped to one resource and one view. */
|
|
414
|
-
resourceComponents?: Record<string, ReactAdminResourceComponents>;
|
|
415
|
-
};
|
|
416
|
-
|
|
417
374
|
const EMPTY_EXTENSIONS: readonly AdminExtension[] = [];
|
|
418
375
|
|
|
419
376
|
/**
|
|
@@ -498,7 +455,7 @@ export function FougereAdmin({
|
|
|
498
455
|
components: resourceComponents?.[resource.name],
|
|
499
456
|
})),
|
|
500
457
|
/*
|
|
501
|
-
* The one page that renders the APP rather than a
|
|
458
|
+
* The one page that renders the APP rather than a facade. It is not a resource —
|
|
502
459
|
* there is no row behind it — so it rides a route, and its data comes from
|
|
503
460
|
* `rpc.topology` rather than from the card.
|
|
504
461
|
*/
|
package/src/theme.tsx
CHANGED
|
@@ -501,7 +501,7 @@ const TopologyIcon = (props: SvgIconProps) => (
|
|
|
501
501
|
);
|
|
502
502
|
|
|
503
503
|
/**
|
|
504
|
-
* The derived menu, plus the one entry that is not a
|
|
504
|
+
* The derived menu, plus the one entry that is not a facade.
|
|
505
505
|
*
|
|
506
506
|
* `Menu.ResourceItems` keeps the resources the card produced — the menu stays derived, and
|
|
507
507
|
* this adds beside it rather than replacing it. The topology entry shows even when nothing
|
package/src/topology-page.tsx
CHANGED
|
@@ -5,18 +5,22 @@
|
|
|
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 {
|
|
21
|
+
import { type Edge, type TopologyReport } from '@fougere/core';
|
|
22
|
+
import { fetchTopology, figureOf, isOpaque, layoutOf, nodesOf } from './Drawing.js';
|
|
23
|
+
import { type TopologyNode } from './TopologyNode.js';
|
|
20
24
|
|
|
21
25
|
/** What the page is: the report, the refusal that means "not observed", or a real failure. */
|
|
22
26
|
type State =
|
|
@@ -60,6 +64,167 @@ function EdgeLine({ edge, direction }: { edge: Edge; direction: 'out' | 'in' }):
|
|
|
60
64
|
);
|
|
61
65
|
}
|
|
62
66
|
|
|
67
|
+
/**
|
|
68
|
+
* A palette path as SVG paint. `sx` resolves `'error.main'` for `color` and `bgcolor` and for
|
|
69
|
+
* nothing else, so `fill` and `stroke` reach CSS verbatim — an invalid value, and SVG falls back
|
|
70
|
+
* to BLACK. Every shape on this page is painted through here.
|
|
71
|
+
*/
|
|
72
|
+
function paint(paths: { fill?: string; stroke?: string }) {
|
|
73
|
+
const read = (theme: Theme, path: string) =>
|
|
74
|
+
path.split('.').reduce<unknown>((held, key) => (held as Record<string, unknown>)?.[key], theme.palette) as string;
|
|
75
|
+
|
|
76
|
+
return (theme: Theme) => ({
|
|
77
|
+
...(paths.fill ? { fill: read(theme, paths.fill) } : {}),
|
|
78
|
+
...(paths.stroke ? { stroke: read(theme, paths.stroke) } : {}),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** What a node is painted with, by the one thing that separates the three states. */
|
|
83
|
+
function toneOf(node: TopologyNode): 'primary' | 'grey' | 'warning' {
|
|
84
|
+
if (node.silent) return 'warning';
|
|
85
|
+
|
|
86
|
+
return node.placement === 'local' ? 'primary' : 'grey';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The system, drawn. Placement is `layoutOf`'s — ranks, order and routing.
|
|
91
|
+
*
|
|
92
|
+
* The nodes are HTML over an SVG that draws only the lines, which is how React Flow is built and
|
|
93
|
+
* why its nodes look like the rest of an app: a card is a `Box` with `sx`, so it reads the theme
|
|
94
|
+
* like every other element. An SVG `<text>` positioned by hand does not.
|
|
95
|
+
*
|
|
96
|
+
* Both layers sit on ONE scale — the container takes the drawing's aspect ratio, and every node is
|
|
97
|
+
* sized AND placed in per cent of it. A size in pixels does not grow with the viewBox the SVG
|
|
98
|
+
* scales to its container, and the lines then stop short of the cards they point at.
|
|
99
|
+
*
|
|
100
|
+
* What the drawing says beyond who reaches whom: thickness is volume, a broken line is declared
|
|
101
|
+
* and never travelled, and refusals are a figure rather than a red line — colour alone excludes a
|
|
102
|
+
* reader who cannot see it, and "how many" is the question a red line raises without answering.
|
|
103
|
+
*/
|
|
104
|
+
function Graph({ nodes, selected, onSelect }: {
|
|
105
|
+
nodes: readonly TopologyNode[];
|
|
106
|
+
selected: string | undefined;
|
|
107
|
+
onSelect: (frond: string) => void;
|
|
108
|
+
}): ReactElement {
|
|
109
|
+
const label = useLabels();
|
|
110
|
+
const drawing = useMemo(() => layoutOf(nodes), [nodes]);
|
|
111
|
+
const per = (value: number, of: number) => `${(value / of) * 100}%`;
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<Card variant="outlined">
|
|
115
|
+
<Box sx={{ position: 'relative', bgcolor: 'action.hover', aspectRatio: `${drawing.width} / ${drawing.height}` }}>
|
|
116
|
+
<Box
|
|
117
|
+
component="svg"
|
|
118
|
+
viewBox={`0 0 ${drawing.width} ${drawing.height}`}
|
|
119
|
+
sx={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}
|
|
120
|
+
>
|
|
121
|
+
<defs>
|
|
122
|
+
<marker id="fougere-arrow" viewBox="0 0 8 8" refX="6.5" refY="4" markerWidth="5" markerHeight="5" orient="auto-start-reverse">
|
|
123
|
+
<Box component="path" d="M 0 0 L 8 4 L 0 8 z" sx={paint({ fill: 'text.disabled' })} />
|
|
124
|
+
</marker>
|
|
125
|
+
</defs>
|
|
126
|
+
|
|
127
|
+
{drawing.edges.map((edge) => (
|
|
128
|
+
<Box
|
|
129
|
+
component="path"
|
|
130
|
+
key={`${edge.from}-${edge.to}`}
|
|
131
|
+
d={edge.path}
|
|
132
|
+
fill="none"
|
|
133
|
+
strokeLinecap="round"
|
|
134
|
+
strokeWidth={edge.weight === undefined ? 1.5 : 1.5 + edge.weight * 3}
|
|
135
|
+
strokeDasharray={edge.weight === undefined ? '2 5' : undefined}
|
|
136
|
+
markerEnd="url(#fougere-arrow)"
|
|
137
|
+
sx={[paint({ stroke: 'text.disabled' }), { strokeOpacity: 0.5 }]}
|
|
138
|
+
/>
|
|
139
|
+
))}
|
|
140
|
+
</Box>
|
|
141
|
+
|
|
142
|
+
{drawing.edges.map((edge) => edge.at && (
|
|
143
|
+
<Box
|
|
144
|
+
key={`figure-${edge.from}-${edge.to}`}
|
|
145
|
+
sx={{
|
|
146
|
+
position: 'absolute',
|
|
147
|
+
left: per(edge.at.x, drawing.width),
|
|
148
|
+
top: per(edge.at.y, drawing.height),
|
|
149
|
+
transform: 'translate(-50%, -50%)',
|
|
150
|
+
px: 0.75,
|
|
151
|
+
borderRadius: 1,
|
|
152
|
+
border: 1,
|
|
153
|
+
borderColor: 'divider',
|
|
154
|
+
bgcolor: 'background.paper',
|
|
155
|
+
fontFamily: 'monospace',
|
|
156
|
+
fontSize: 11,
|
|
157
|
+
fontWeight: 600,
|
|
158
|
+
lineHeight: '17px',
|
|
159
|
+
whiteSpace: 'nowrap',
|
|
160
|
+
color: edge.errors > 0 ? 'error.main' : 'text.secondary',
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
163
|
+
{edge.count === undefined ? label('topology.noTraffic', 'no traffic') : figureOf(edge)}
|
|
164
|
+
</Box>
|
|
165
|
+
))}
|
|
166
|
+
|
|
167
|
+
{drawing.nodes.map(({ node, x, y, width, height }) => {
|
|
168
|
+
const tone = toneOf(node);
|
|
169
|
+
return (
|
|
170
|
+
<Box
|
|
171
|
+
key={node.frond}
|
|
172
|
+
onClick={() => onSelect(node.frond)}
|
|
173
|
+
sx={{
|
|
174
|
+
position: 'absolute',
|
|
175
|
+
left: per(x, drawing.width),
|
|
176
|
+
top: per(y, drawing.height),
|
|
177
|
+
width: per(width, drawing.width),
|
|
178
|
+
height: per(height, drawing.height),
|
|
179
|
+
transform: 'translate(-50%, -50%)',
|
|
180
|
+
display: 'flex',
|
|
181
|
+
flexDirection: 'column',
|
|
182
|
+
justifyContent: 'center',
|
|
183
|
+
gap: 0.25,
|
|
184
|
+
overflow: 'hidden',
|
|
185
|
+
px: 1.25,
|
|
186
|
+
borderRadius: 1.5,
|
|
187
|
+
border: node.frond === selected ? 2 : 1.4,
|
|
188
|
+
borderStyle: node.silent ? 'dashed' : 'solid',
|
|
189
|
+
borderColor: tone === 'grey' ? 'divider' : `${tone}.main`,
|
|
190
|
+
bgcolor: 'background.paper',
|
|
191
|
+
cursor: 'pointer',
|
|
192
|
+
}}
|
|
193
|
+
>
|
|
194
|
+
<Box sx={{ fontFamily: 'monospace', fontSize: 13.5, fontWeight: 600 }}>{node.frond}</Box>
|
|
195
|
+
<Row>
|
|
196
|
+
{/* A remote's two zeroes are not a measurement: its shape is published by the
|
|
197
|
+
process that owns it, so the card says WHERE it is instead of counting. */}
|
|
198
|
+
{!node.silent && (
|
|
199
|
+
<Typography variant="caption" color="text.secondary" sx={{ fontSize: 10.5 }}>
|
|
200
|
+
{isOpaque(node)
|
|
201
|
+
? label('topology.elsewhere', 'elsewhere')
|
|
202
|
+
: `${node.entities} · ${node.facades}`}
|
|
203
|
+
</Typography>
|
|
204
|
+
)}
|
|
205
|
+
{tone !== 'grey' && (
|
|
206
|
+
<Chip
|
|
207
|
+
size="small"
|
|
208
|
+
color={tone}
|
|
209
|
+
variant="outlined"
|
|
210
|
+
sx={{ height: 16, '& .MuiChip-label': { px: 0.75, fontSize: 9.5, letterSpacing: '0.05em' } }}
|
|
211
|
+
label={node.silent ? label('topology.unheard', 'unheard') : label('topology.here', 'here')}
|
|
212
|
+
/>
|
|
213
|
+
)}
|
|
214
|
+
</Row>
|
|
215
|
+
</Box>
|
|
216
|
+
);
|
|
217
|
+
})}
|
|
218
|
+
</Box>
|
|
219
|
+
<Box sx={{ px: 2, py: 1.25 }}>
|
|
220
|
+
<Typography variant="caption" color="text.secondary">
|
|
221
|
+
{label('topology.legend', 'Thickness is volume. A broken line is declared and never travelled. Click a frond for what it holds.')}
|
|
222
|
+
</Typography>
|
|
223
|
+
</Box>
|
|
224
|
+
</Card>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
63
228
|
function FrondCard({ node }: { node: TopologyNode }): ReactElement {
|
|
64
229
|
const label = useLabels();
|
|
65
230
|
const here = node.placement === 'local';
|
|
@@ -70,8 +235,10 @@ function FrondCard({ node }: { node: TopologyNode }): ReactElement {
|
|
|
70
235
|
<Typography variant="h6" sx={{ fontWeight: 700 }}>{node.frond}</Typography>
|
|
71
236
|
<Chip
|
|
72
237
|
size="small"
|
|
73
|
-
color={here ? 'primary' : 'default'}
|
|
74
|
-
label={
|
|
238
|
+
color={node.silent ? 'warning' : here ? 'primary' : 'default'}
|
|
239
|
+
label={node.silent
|
|
240
|
+
? label('topology.unheard', 'declared, unheard')
|
|
241
|
+
: here ? label('topology.here', 'here') : label('topology.elsewhere', 'elsewhere')}
|
|
75
242
|
/>
|
|
76
243
|
</Row>
|
|
77
244
|
{/*
|
|
@@ -80,10 +247,12 @@ function FrondCard({ node }: { node: TopologyNode }): ReactElement {
|
|
|
80
247
|
empty frond, which reads as a frond with nothing in it.
|
|
81
248
|
*/}
|
|
82
249
|
<Typography variant="body2" color="text.secondary">
|
|
83
|
-
{
|
|
84
|
-
? label('topology.
|
|
85
|
-
:
|
|
86
|
-
|
|
250
|
+
{node.silent
|
|
251
|
+
? label('topology.silent', `Declared at ${node.at ?? 'no address'} — nothing has answered from it.`, { at: node.at ?? '' })
|
|
252
|
+
: isOpaque(node)
|
|
253
|
+
? label('topology.opaque', 'Its shape is published by the process that owns it.')
|
|
254
|
+
: `${label('topology.entities', `${node.entities} entities`, { smart_count: node.entities })}`
|
|
255
|
+
+ ` · ${label('topology.facades', `${node.facades} facades`, { smart_count: node.facades })}`}
|
|
87
256
|
</Typography>
|
|
88
257
|
{node.calls.map((edge) => <EdgeLine key={`out-${edge.to}`} edge={edge} direction="out" />)}
|
|
89
258
|
{node.calledBy.map((edge) => <EdgeLine key={`in-${edge.from}`} edge={edge} direction="in" />)}
|
|
@@ -100,6 +269,8 @@ export interface FougereTopologyProps {
|
|
|
100
269
|
export function FougereTopology({ endpoint = CALL_ENDPOINT, fetcher = browserFetcher }: FougereTopologyProps = {}): ReactElement {
|
|
101
270
|
const label = useLabels();
|
|
102
271
|
const [state, setState] = useState<State>({ status: 'loading' });
|
|
272
|
+
/** Which frond the detail below is about — the drawing answers "where", one card answers "what". */
|
|
273
|
+
const [selected, setSelected] = useState<string>();
|
|
103
274
|
|
|
104
275
|
useEffect(() => {
|
|
105
276
|
let live = true;
|
|
@@ -154,18 +325,34 @@ export function FougereTopology({ endpoint = CALL_ENDPOINT, fetcher = browserFet
|
|
|
154
325
|
}
|
|
155
326
|
|
|
156
327
|
const nodes = nodesOf(state.report);
|
|
157
|
-
|
|
328
|
+
// Three counts because there are three ways of being known, and `elsewhere` means ANSWERED
|
|
329
|
+
// from elsewhere: counting a frond that never answered among them makes the summary
|
|
330
|
+
// contradict the card under it.
|
|
331
|
+
const unheard = nodes.filter((node) => node.silent).length;
|
|
332
|
+
const elsewhere = nodes.filter((node) => node.placement === 'remote' && !node.silent).length;
|
|
333
|
+
const shown = nodes.find((node) => node.frond === selected) ?? nodes[0];
|
|
334
|
+
const travelled = new Set(state.report.edges.map((edge) => `${edge.from} ${edge.to}`));
|
|
335
|
+
const quiet = state.report.declared.edges.filter((edge) => !travelled.has(`${edge.from} ${edge.to}`)).length;
|
|
158
336
|
|
|
159
337
|
return (
|
|
160
338
|
<Box sx={{ p: 3, display: 'grid', gap: 2 }}>
|
|
161
339
|
{title}
|
|
162
340
|
<Row>
|
|
163
341
|
<Typography variant="body2" color="text.secondary">
|
|
164
|
-
{label('topology.here_count', `${nodes.length - elsewhere} here`, { smart_count: nodes.length - elsewhere })}
|
|
342
|
+
{label('topology.here_count', `${nodes.length - elsewhere - unheard} here`, { smart_count: nodes.length - elsewhere - unheard })}
|
|
165
343
|
{' · '}
|
|
166
344
|
{label('topology.elsewhere_count', `${elsewhere} elsewhere`, { smart_count: elsewhere })}
|
|
345
|
+
{unheard > 0 && <>
|
|
346
|
+
{' · '}
|
|
347
|
+
{label('topology.unheard_count', `${unheard} unheard`, { smart_count: unheard })}
|
|
348
|
+
</>}
|
|
167
349
|
{' · '}
|
|
168
350
|
{label('topology.paths', `${state.report.edges.length} observed call paths`, { smart_count: state.report.edges.length })}
|
|
351
|
+
{/* The two anomalies, counted where a reader looks first — not left to be found in the drawing. */}
|
|
352
|
+
{quiet > 0 && <>
|
|
353
|
+
{' · '}
|
|
354
|
+
{label('topology.quiet_count', `${quiet} link without traffic`, { smart_count: quiet })}
|
|
355
|
+
</>}
|
|
169
356
|
</Typography>
|
|
170
357
|
{/* Saturation: the one signal a static shape cannot carry. */}
|
|
171
358
|
{state.report.active > 0 && (
|
|
@@ -176,9 +363,8 @@ export function FougereTopology({ endpoint = CALL_ENDPOINT, fetcher = browserFet
|
|
|
176
363
|
/>
|
|
177
364
|
)}
|
|
178
365
|
</Row>
|
|
179
|
-
<
|
|
180
|
-
|
|
181
|
-
</Box>
|
|
366
|
+
<Graph nodes={nodes} selected={shown?.frond} onSelect={setSelected} />
|
|
367
|
+
{shown && <Box sx={{ maxWidth: 520 }}><FrondCard node={shown} /></Box>}
|
|
182
368
|
{/*
|
|
183
369
|
An edge is only knowable on the side that made the call, so a process with no
|
|
184
370
|
outgoing call has none — that is ordinary, not a gap in the reading.
|