@eventcatalog/core 4.9.0 → 4.10.1
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/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-HKVOSQTD.js → chunk-OC2RGFPU.js} +1 -1
- package/dist/{chunk-MPHX5C7M.js → chunk-PHG7KEUH.js} +1 -1
- package/dist/{chunk-LVGQ23LR.js → chunk-QIV7LWVJ.js} +1 -1
- package/dist/{chunk-XQHS36GO.js → chunk-SQNVL7PI.js} +1 -1
- package/dist/{chunk-3CGSSMTK.js → chunk-T2HLSITU.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +5 -5
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/astro.config.mjs +6 -0
- package/eventcatalog/src/components/Badge.astro +3 -1
- package/eventcatalog/src/components/CatalogGraph/CatalogForceGraph.tsx +260 -175
- package/eventcatalog/src/components/FieldsExplorer/FieldNodeGraph.tsx +0 -5
- package/eventcatalog/src/components/MDX/ArchitectureGraph/ArchitectureGraph.astro +62 -0
- package/eventcatalog/src/components/MDX/ArchitectureGraph/ArchitectureGraphPortal.tsx +22 -0
- package/eventcatalog/src/components/MDX/ArchitectureGraph/AstroArchitectureGraph.tsx +42 -0
- package/eventcatalog/src/components/MDX/Design/Design.astro +2 -0
- package/eventcatalog/src/components/MDX/EntityMap/EntityMap.astro +2 -0
- package/eventcatalog/src/components/MDX/Flow/Flow.astro +2 -0
- package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +5 -7
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +5 -0
- package/eventcatalog/src/components/MDX/components.tsx +13 -0
- package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +121 -88
- package/eventcatalog/src/components/SideNav/NestedSideBar/storage.ts +55 -7
- package/eventcatalog/src/components/SideNav/NestedSideBar/utils.spec.ts +59 -15
- package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +53 -7
- package/eventcatalog/src/content.config.ts +70 -0
- package/eventcatalog/src/enterprise/collections/resource-docs-utils.ts +19 -0
- package/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro +46 -1
- package/eventcatalog/src/enterprise/fields/pages/fields.astro +2 -0
- package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +1 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/[docVersion]/index.astro +16 -15
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/[docType]/[docId]/index.astro +21 -17
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +1 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +45 -4
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +16 -8
- package/eventcatalog/src/pages/docs/[type]/[id]/language/[dictionaryId]/index.astro +48 -65
- package/eventcatalog/src/pages/docs/[type]/[id]/language/index.astro +45 -18
- package/eventcatalog/src/pages/docs/teams/[id]/index.astro +1 -1
- package/eventcatalog/src/pages/docs/users/[id]/index.astro +1 -1
- package/eventcatalog/src/pages/index.astro +34 -2
- package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +5 -1
- package/eventcatalog/src/pages/triggers/[type]/[id]/[version]/index.astro +2 -0
- package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +2 -0
- package/eventcatalog/src/pages/visualiser/graph/index.astro +2 -23
- package/eventcatalog/src/stores/sidebar-store/builders/adr.ts +96 -34
- package/eventcatalog/src/stores/sidebar-store/builders/agent.ts +167 -75
- package/eventcatalog/src/stores/sidebar-store/builders/container.ts +135 -59
- package/eventcatalog/src/stores/sidebar-store/builders/data-product.ts +144 -58
- package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +306 -188
- package/eventcatalog/src/stores/sidebar-store/builders/entity.ts +108 -39
- package/eventcatalog/src/stores/sidebar-store/builders/flow.ts +147 -63
- package/eventcatalog/src/stores/sidebar-store/builders/message.ts +212 -121
- package/eventcatalog/src/stores/sidebar-store/builders/service.ts +223 -127
- package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +3 -23
- package/eventcatalog/src/stores/sidebar-store/builders/system.ts +187 -97
- package/eventcatalog/src/stores/sidebar-store/custom-sidebar.ts +678 -0
- package/eventcatalog/src/stores/sidebar-store/state.ts +66 -36
- package/eventcatalog/src/styles/tailwind.css +82 -0
- package/eventcatalog/src/utils/collections/resource-docs.ts +1 -0
- package/eventcatalog/src/utils/node-graphs/catalog-force-graph.ts +41 -0
- package/package.json +1 -1
|
@@ -30,23 +30,40 @@ import { drag } from 'd3-drag';
|
|
|
30
30
|
import { getColorForCollection, tailwind500RgbByColor } from '@utils/collection-colors';
|
|
31
31
|
import { getIconForCollection } from '@utils/collections/icons';
|
|
32
32
|
import { buildUrl } from '@utils/url-builder';
|
|
33
|
+
import type { CatalogGraphWireLink, CatalogGraphWireNode } from '@utils/node-graphs/catalog-force-graph';
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
export interface CatalogGraphWireNode {
|
|
36
|
-
/** Resource id, e.g. `InventoryService` */
|
|
37
|
-
id: string;
|
|
38
|
-
label: string;
|
|
39
|
-
collection: string;
|
|
40
|
-
version?: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** [sourceIndex, targetIndex, labelIndex] into the nodes / linkLabels arrays */
|
|
44
|
-
export type CatalogGraphWireLink = [number, number, number];
|
|
35
|
+
export type { CatalogGraphWireLink, CatalogGraphWireNode };
|
|
45
36
|
|
|
46
37
|
interface Props {
|
|
47
38
|
nodes: CatalogGraphWireNode[];
|
|
48
39
|
links: CatalogGraphWireLink[];
|
|
49
40
|
linkLabels: string[];
|
|
41
|
+
/** Seed the initial lens. URL state wins when `syncUrl` is on. */
|
|
42
|
+
initialLens?: string;
|
|
43
|
+
/** Seed the initial focused node key, e.g. `domains/Orders`. URL state wins when `syncUrl` is on. */
|
|
44
|
+
initialFocus?: string;
|
|
45
|
+
/** Seed the initial focus depth (1–3). URL state wins when `syncUrl` is on. */
|
|
46
|
+
initialFocusDepth?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Read the view state from the URL and mirror changes back into it — the
|
|
49
|
+
* standalone /visualiser/graph page. Off for graphs embedded in docs pages,
|
|
50
|
+
* which must not rewrite their host page's query string.
|
|
51
|
+
*/
|
|
52
|
+
syncUrl?: boolean;
|
|
53
|
+
/** Show the lens picker / detail slider (hidden on embedded graphs, which pin a lens) */
|
|
54
|
+
showLensPicker?: boolean;
|
|
55
|
+
showSearch?: boolean;
|
|
56
|
+
showLegend?: boolean;
|
|
57
|
+
/** When off, plain scrolling over the canvas scrolls the page — zooming needs ctrl/cmd+scroll */
|
|
58
|
+
zoomOnScroll?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Base URL of the standalone /visualiser/graph page. When set, an "open"
|
|
61
|
+
* link is rendered whose query string carries the CURRENT view state (focus,
|
|
62
|
+
* depth, lens, hidden collections), so the full page opens on exactly the
|
|
63
|
+
* view the embedded graph is showing.
|
|
64
|
+
*/
|
|
65
|
+
openInGraphUrl?: string;
|
|
66
|
+
openInGraphLabel?: string;
|
|
50
67
|
}
|
|
51
68
|
|
|
52
69
|
interface GraphNode extends CatalogGraphWireNode {
|
|
@@ -243,7 +260,21 @@ const distanceToHull = (px: number, py: number, hull: [number, number][]) => {
|
|
|
243
260
|
return min;
|
|
244
261
|
};
|
|
245
262
|
|
|
246
|
-
const CatalogForceGraph = ({
|
|
263
|
+
const CatalogForceGraph = ({
|
|
264
|
+
nodes,
|
|
265
|
+
links,
|
|
266
|
+
linkLabels,
|
|
267
|
+
initialLens,
|
|
268
|
+
initialFocus,
|
|
269
|
+
initialFocusDepth,
|
|
270
|
+
syncUrl = true,
|
|
271
|
+
showLensPicker = true,
|
|
272
|
+
openInGraphUrl,
|
|
273
|
+
openInGraphLabel,
|
|
274
|
+
showSearch = true,
|
|
275
|
+
showLegend = true,
|
|
276
|
+
zoomOnScroll = true,
|
|
277
|
+
}: Props) => {
|
|
247
278
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
248
279
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
|
249
280
|
const tooltipRef = useRef<HTMLDivElement>(null);
|
|
@@ -255,37 +286,58 @@ const CatalogForceGraph = ({ nodes, links, linkLabels }: Props) => {
|
|
|
255
286
|
const iconSpritesRef = useRef(new Map<string, HTMLCanvasElement>());
|
|
256
287
|
const drawRef = useRef<() => void>(() => {});
|
|
257
288
|
// View state initialises from the URL so people can share what they're looking
|
|
258
|
-
// at (?lens=domains&focus=services/OrderService&depth=2&detail=3&hide=teams,entities)
|
|
289
|
+
// at (?lens=domains&focus=services/OrderService&depth=2&detail=3&hide=teams,entities).
|
|
290
|
+
// Embedded graphs (syncUrl off) seed from the initial* props instead — their
|
|
291
|
+
// host page's query string is not theirs to read or write.
|
|
292
|
+
const urlParam = (key: string) => (syncUrl ? new URLSearchParams(window.location.search).get(key) : null);
|
|
259
293
|
const [hiddenCollections, setHiddenCollections] = useState<Set<string>>(() => {
|
|
260
|
-
const hide =
|
|
294
|
+
const hide = urlParam('hide');
|
|
261
295
|
return new Set(hide ? hide.split(',').filter(Boolean) : []);
|
|
262
296
|
});
|
|
263
297
|
const [lensKey, setLensKey] = useState(() => {
|
|
264
|
-
const lens =
|
|
298
|
+
const lens = urlParam('lens') ?? initialLens;
|
|
265
299
|
return lens && lens in LENSES ? lens : 'all';
|
|
266
300
|
});
|
|
267
|
-
const [focusKey, setFocusKey] = useState<string | null>(() =>
|
|
301
|
+
const [focusKey, setFocusKey] = useState<string | null>(() => urlParam('focus') ?? initialFocus ?? null);
|
|
268
302
|
const [focusDepth, setFocusDepth] = useState(() =>
|
|
269
|
-
Math.min(3, Math.max(1, Number(
|
|
303
|
+
Math.min(3, Math.max(1, Number(urlParam('depth')) || initialFocusDepth || 1))
|
|
270
304
|
);
|
|
271
305
|
// How many hops of detail radiate out from a lens's anchor nodes (4 = everything)
|
|
272
|
-
const [lensDepth, setLensDepth] = useState(() =>
|
|
273
|
-
Math.min(MAX_LENS_DEPTH, Math.max(1, Number(new URLSearchParams(window.location.search).get('detail')) || 1))
|
|
274
|
-
);
|
|
306
|
+
const [lensDepth, setLensDepth] = useState(() => Math.min(MAX_LENS_DEPTH, Math.max(1, Number(urlParam('detail')) || 1)));
|
|
275
307
|
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
const params = new URLSearchParams(window.location.search);
|
|
308
|
+
// Serialise the current view state into query params — shared between the
|
|
309
|
+
// URL sync below and the embedded graphs' "open in graph" link
|
|
310
|
+
const applyViewParams = (params: URLSearchParams) => {
|
|
280
311
|
const setOrDelete = (key: string, value: string | null) => (value ? params.set(key, value) : params.delete(key));
|
|
281
312
|
setOrDelete('lens', lensKey !== 'all' ? lensKey : null);
|
|
282
313
|
setOrDelete('focus', focusKey);
|
|
283
314
|
setOrDelete('depth', focusKey && focusDepth !== 1 ? String(focusDepth) : null);
|
|
284
315
|
setOrDelete('detail', lensDepth !== 1 ? String(lensDepth) : null);
|
|
285
316
|
setOrDelete('hide', hiddenCollections.size > 0 ? [...hiddenCollections].sort().join(',') : null);
|
|
286
|
-
|
|
317
|
+
return params;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
// Reflect the view state back into the URL (replaceState, so no history spam);
|
|
321
|
+
// unrelated params — e.g. the stress page's ?nodes — are preserved
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
if (!syncUrl) return;
|
|
324
|
+
const query = applyViewParams(new URLSearchParams(window.location.search)).toString();
|
|
287
325
|
window.history.replaceState(null, '', `${window.location.pathname}${query ? `?${query}` : ''}${window.location.hash}`);
|
|
288
326
|
}, [lensKey, focusKey, focusDepth, lensDepth, hiddenCollections]);
|
|
327
|
+
|
|
328
|
+
// The "open in graph" link tracks the live view, so what opens is what's shown
|
|
329
|
+
const openInGraphHref = (() => {
|
|
330
|
+
if (!openInGraphUrl) return undefined;
|
|
331
|
+
const query = applyViewParams(new URLSearchParams()).toString();
|
|
332
|
+
return `${openInGraphUrl}${query ? `?${query}` : ''}`;
|
|
333
|
+
})();
|
|
334
|
+
// The island hydrates client-only, so the graph would otherwise pop in — fade
|
|
335
|
+
// it in on mount instead (the rAF lets the opacity-0 frame paint first)
|
|
336
|
+
const [visible, setVisible] = useState(false);
|
|
337
|
+
useEffect(() => {
|
|
338
|
+
const raf = requestAnimationFrame(() => setVisible(true));
|
|
339
|
+
return () => cancelAnimationFrame(raf);
|
|
340
|
+
}, []);
|
|
289
341
|
const [searchValue, setSearchValue] = useState('');
|
|
290
342
|
const [showSuggestions, setShowSuggestions] = useState(false);
|
|
291
343
|
const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1);
|
|
@@ -985,10 +1037,15 @@ const CatalogForceGraph = ({ nodes, links, linkLabels }: Props) => {
|
|
|
985
1037
|
.scaleExtent([0.1, 4])
|
|
986
1038
|
// Let node drags win over panning: ignore presses that start on a node
|
|
987
1039
|
.filter((event) => {
|
|
1040
|
+
// Embedded graphs must not trap touch scrolling — a one-finger swipe over
|
|
1041
|
+
// the canvas scrolls the page; a two-finger gesture pans/zooms the graph
|
|
1042
|
+
if (event.type === 'touchstart' && !zoomOnScroll && event.touches.length < 2) return false;
|
|
988
1043
|
if (event.type === 'mousedown' || event.type === 'touchstart') {
|
|
989
1044
|
const [x, y] = pointerPosition(event);
|
|
990
1045
|
return !findNode(x, y);
|
|
991
1046
|
}
|
|
1047
|
+
// Embedded graphs must not hijack page scrolling — zooming needs ctrl/cmd there
|
|
1048
|
+
if (event.type === 'wheel' && !zoomOnScroll && !event.ctrlKey && !event.metaKey) return false;
|
|
992
1049
|
return !event.ctrlKey || event.type === 'wheel';
|
|
993
1050
|
})
|
|
994
1051
|
.on('zoom', (event) => {
|
|
@@ -1000,6 +1057,9 @@ const CatalogForceGraph = ({ nodes, links, linkLabels }: Props) => {
|
|
|
1000
1057
|
});
|
|
1001
1058
|
|
|
1002
1059
|
const dragBehaviour = drag<HTMLCanvasElement, unknown>()
|
|
1060
|
+
// On embeds, touch node-drags would also swallow page swipes that happen to
|
|
1061
|
+
// start on a node — leave touch to the page there (tap-to-focus still works)
|
|
1062
|
+
.filter((event) => (zoomOnScroll || event.type !== 'touchstart') && !event.ctrlKey && !event.button)
|
|
1003
1063
|
.subject((event) => {
|
|
1004
1064
|
const [x, y] = pointerPosition(event.sourceEvent);
|
|
1005
1065
|
const node = findNode(x, y);
|
|
@@ -1171,7 +1231,7 @@ const CatalogForceGraph = ({ nodes, links, linkLabels }: Props) => {
|
|
|
1171
1231
|
resizeObserver.disconnect();
|
|
1172
1232
|
themeObserver.disconnect();
|
|
1173
1233
|
};
|
|
1174
|
-
}, [viewNodes, viewLinks, lens, hiddenCollections, clusterAssignments, focusKey, focusAncestors]);
|
|
1234
|
+
}, [viewNodes, viewLinks, lens, hiddenCollections, clusterAssignments, focusKey, focusAncestors, zoomOnScroll]);
|
|
1175
1235
|
|
|
1176
1236
|
const toggleCollection = (collection: string) => {
|
|
1177
1237
|
setHiddenCollections((current) => {
|
|
@@ -1185,7 +1245,10 @@ const CatalogForceGraph = ({ nodes, links, linkLabels }: Props) => {
|
|
|
1185
1245
|
const focusedNode = focusKey ? graph.nodes.find((n) => n.key === focusKey) : undefined;
|
|
1186
1246
|
|
|
1187
1247
|
return (
|
|
1188
|
-
<div
|
|
1248
|
+
<div
|
|
1249
|
+
ref={containerRef}
|
|
1250
|
+
className={`relative h-full w-full overflow-hidden motion-safe:transition-opacity motion-safe:duration-500 ${visible ? 'opacity-100' : 'opacity-0'}`}
|
|
1251
|
+
>
|
|
1189
1252
|
{/* Hidden icon sources: React renders them once, the sprite effect rasterises
|
|
1190
1253
|
them for the canvas. Both lucide (color prop) and heroicons (currentColor
|
|
1191
1254
|
via style) end up with white strokes. */}
|
|
@@ -1205,138 +1268,158 @@ const CatalogForceGraph = ({ nodes, links, linkLabels }: Props) => {
|
|
|
1205
1268
|
className="pointer-events-none absolute z-10 rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] shadow-md transition-opacity duration-100"
|
|
1206
1269
|
style={{ opacity: 0 }}
|
|
1207
1270
|
/>
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1271
|
+
{/* Top-left chrome stacks in one column so the link and lens picker never overlap */}
|
|
1272
|
+
<div className="absolute left-3 top-3 z-10 flex flex-col items-start gap-2">
|
|
1273
|
+
{showLensPicker && (
|
|
1274
|
+
<div className="flex flex-col gap-2 rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] px-3 py-2 text-xs shadow-xs">
|
|
1275
|
+
<div className="flex items-center gap-2">
|
|
1276
|
+
<label htmlFor="catalog-graph-lens" className="font-semibold text-[rgb(var(--ec-page-text))]">
|
|
1277
|
+
Lens
|
|
1278
|
+
</label>
|
|
1279
|
+
<select
|
|
1280
|
+
id="catalog-graph-lens"
|
|
1281
|
+
value={lensKey}
|
|
1282
|
+
onChange={(event) => {
|
|
1283
|
+
// A new lens is a fresh view — drop any focus and legend filters
|
|
1284
|
+
setLensKey(event.target.value);
|
|
1285
|
+
setFocusKey(null);
|
|
1286
|
+
setFocusDepth(1);
|
|
1287
|
+
setHiddenCollections(new Set());
|
|
1288
|
+
}}
|
|
1289
|
+
title={lens.description}
|
|
1290
|
+
className="rounded-sm border border-[rgb(var(--ec-input-border))] bg-[rgb(var(--ec-input-bg))] px-2 py-1 text-[rgb(var(--ec-input-text))]"
|
|
1291
|
+
>
|
|
1292
|
+
{Object.entries(LENSES).map(([key, { label }]) => (
|
|
1293
|
+
<option key={key} value={key}>
|
|
1294
|
+
{label}
|
|
1295
|
+
</option>
|
|
1296
|
+
))}
|
|
1297
|
+
</select>
|
|
1298
|
+
</div>
|
|
1299
|
+
{lens.hubCollections && !focusedNode && (
|
|
1300
|
+
<label
|
|
1301
|
+
className="flex items-center gap-2 text-[rgb(var(--ec-page-text-muted))]"
|
|
1302
|
+
title="How many relationship hops to show around the anchor nodes"
|
|
1303
|
+
>
|
|
1304
|
+
Detail
|
|
1305
|
+
<input
|
|
1306
|
+
type="range"
|
|
1307
|
+
min={1}
|
|
1308
|
+
max={MAX_LENS_DEPTH}
|
|
1309
|
+
step={1}
|
|
1310
|
+
value={lensDepth}
|
|
1311
|
+
onChange={(event) => setLensDepth(Number(event.target.value))}
|
|
1312
|
+
className="w-24 accent-[rgb(var(--ec-accent))]"
|
|
1313
|
+
/>
|
|
1314
|
+
<span className="w-5 text-[rgb(var(--ec-page-text))]">{lensDepth >= MAX_LENS_DEPTH ? 'All' : lensDepth}</span>
|
|
1315
|
+
</label>
|
|
1316
|
+
)}
|
|
1317
|
+
</div>
|
|
1318
|
+
)}
|
|
1319
|
+
{openInGraphHref && (
|
|
1320
|
+
<a
|
|
1321
|
+
href={openInGraphHref}
|
|
1322
|
+
className="rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] shadow-xs hover:bg-[rgb(var(--ec-accent-subtle))]"
|
|
1237
1323
|
>
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
type="range"
|
|
1241
|
-
min={1}
|
|
1242
|
-
max={MAX_LENS_DEPTH}
|
|
1243
|
-
step={1}
|
|
1244
|
-
value={lensDepth}
|
|
1245
|
-
onChange={(event) => setLensDepth(Number(event.target.value))}
|
|
1246
|
-
className="w-24 accent-[rgb(var(--ec-accent))]"
|
|
1247
|
-
/>
|
|
1248
|
-
<span className="w-5 text-[rgb(var(--ec-page-text))]">{lensDepth >= MAX_LENS_DEPTH ? 'All' : lensDepth}</span>
|
|
1249
|
-
</label>
|
|
1324
|
+
{openInGraphLabel ?? 'Open full screen'} →
|
|
1325
|
+
</a>
|
|
1250
1326
|
)}
|
|
1251
1327
|
</div>
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
<span className="
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1328
|
+
{(showSearch || focusedNode) && (
|
|
1329
|
+
// z-20: the search dropdown must paint over the Resources legend below it
|
|
1330
|
+
<div className="absolute right-3 top-3 z-20 flex flex-col gap-2 rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] px-3 py-2 text-xs shadow-xs">
|
|
1331
|
+
{showSearch && (
|
|
1332
|
+
<div ref={searchContainerRef} className="relative">
|
|
1333
|
+
<input
|
|
1334
|
+
type="text"
|
|
1335
|
+
placeholder="Search & focus…"
|
|
1336
|
+
value={searchValue}
|
|
1337
|
+
onChange={(event) => {
|
|
1338
|
+
setSearchValue(event.target.value);
|
|
1339
|
+
setShowSuggestions(true);
|
|
1340
|
+
setSelectedSuggestionIndex(-1);
|
|
1341
|
+
}}
|
|
1342
|
+
onFocus={() => setShowSuggestions(true)}
|
|
1343
|
+
onKeyDown={handleSearchKeyDown}
|
|
1344
|
+
className="w-56 rounded-sm border border-[rgb(var(--ec-input-border))] bg-[rgb(var(--ec-input-bg))] py-1 pl-2 pr-7 text-[rgb(var(--ec-input-text))] placeholder:text-[rgb(var(--ec-page-text-muted))] focus:outline-none focus:ring-2 focus:ring-[rgb(var(--ec-accent))]"
|
|
1345
|
+
/>
|
|
1346
|
+
{searchValue && (
|
|
1347
|
+
<button
|
|
1348
|
+
type="button"
|
|
1349
|
+
onClick={() => setSearchValue('')}
|
|
1350
|
+
aria-label="Clear search"
|
|
1351
|
+
className="absolute right-1.5 top-1/2 -translate-y-1/2 text-[rgb(var(--ec-page-text-muted))] hover:text-[rgb(var(--ec-page-text))]"
|
|
1352
|
+
>
|
|
1353
|
+
×
|
|
1354
|
+
</button>
|
|
1355
|
+
)}
|
|
1356
|
+
{showSuggestions && searchSuggestions.length > 0 && (
|
|
1357
|
+
<div className="absolute left-0 right-0 top-full z-50 mt-1 max-h-64 overflow-y-auto rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] shadow-lg">
|
|
1358
|
+
{searchSuggestions.map((node, index) => {
|
|
1359
|
+
const Icon = getIconForCollection(node.collection);
|
|
1360
|
+
const chipStyle = collectionChipStyle(node.collection);
|
|
1361
|
+
const isSelected = index === selectedSuggestionIndex;
|
|
1362
|
+
return (
|
|
1363
|
+
<button
|
|
1364
|
+
key={node.key}
|
|
1365
|
+
type="button"
|
|
1366
|
+
onClick={() => selectSuggestion(node)}
|
|
1367
|
+
onMouseEnter={() => setSelectedSuggestionIndex(index)}
|
|
1368
|
+
className={`flex w-full items-center gap-2 px-2 py-1.5 text-left ${isSelected ? 'bg-[rgb(var(--ec-accent-subtle))]' : ''}`}
|
|
1369
|
+
>
|
|
1370
|
+
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md" style={chipStyle}>
|
|
1371
|
+
<Icon width={13} height={13} />
|
|
1372
|
+
</span>
|
|
1373
|
+
<span className="min-w-0 flex-1">
|
|
1374
|
+
<span className="block truncate font-medium text-[rgb(var(--ec-page-text))]">{node.label}</span>
|
|
1375
|
+
{node.version && (
|
|
1376
|
+
<span className="block truncate text-[10px] text-[rgb(var(--ec-page-text-muted))]">
|
|
1377
|
+
v{node.version}
|
|
1378
|
+
</span>
|
|
1379
|
+
)}
|
|
1380
|
+
</span>
|
|
1381
|
+
<span className="shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium" style={chipStyle}>
|
|
1382
|
+
{COLLECTION_LABELS[node.collection] ?? node.collection}
|
|
1383
|
+
</span>
|
|
1384
|
+
</button>
|
|
1385
|
+
);
|
|
1386
|
+
})}
|
|
1387
|
+
</div>
|
|
1388
|
+
)}
|
|
1306
1389
|
</div>
|
|
1307
1390
|
)}
|
|
1391
|
+
{focusedNode && (
|
|
1392
|
+
<>
|
|
1393
|
+
<button
|
|
1394
|
+
type="button"
|
|
1395
|
+
onClick={() => setFocusKey(null)}
|
|
1396
|
+
className="flex items-center gap-1 self-start rounded-full border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-accent-subtle))] px-2 py-0.5 text-[rgb(var(--ec-page-text))] hover:opacity-80"
|
|
1397
|
+
title="Clear focus"
|
|
1398
|
+
>
|
|
1399
|
+
<span
|
|
1400
|
+
className="inline-block h-2 w-2 rounded-full"
|
|
1401
|
+
style={{ backgroundColor: nodeColor(focusedNode.collection) }}
|
|
1402
|
+
/>
|
|
1403
|
+
Focused: {focusedNode.label}
|
|
1404
|
+
<span aria-hidden="true">×</span>
|
|
1405
|
+
</button>
|
|
1406
|
+
<label className="flex items-center gap-2 text-[rgb(var(--ec-page-text-muted))]">
|
|
1407
|
+
Depth
|
|
1408
|
+
<input
|
|
1409
|
+
type="range"
|
|
1410
|
+
min={1}
|
|
1411
|
+
max={3}
|
|
1412
|
+
step={1}
|
|
1413
|
+
value={focusDepth}
|
|
1414
|
+
onChange={(event) => setFocusDepth(Number(event.target.value))}
|
|
1415
|
+
className="w-24 accent-[rgb(var(--ec-accent))]"
|
|
1416
|
+
/>
|
|
1417
|
+
<span className="w-3 text-[rgb(var(--ec-page-text))]">{focusDepth}</span>
|
|
1418
|
+
</label>
|
|
1419
|
+
</>
|
|
1420
|
+
)}
|
|
1308
1421
|
</div>
|
|
1309
|
-
|
|
1310
|
-
<>
|
|
1311
|
-
<button
|
|
1312
|
-
type="button"
|
|
1313
|
-
onClick={() => setFocusKey(null)}
|
|
1314
|
-
className="flex items-center gap-1 self-start rounded-full border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-accent-subtle))] px-2 py-0.5 text-[rgb(var(--ec-page-text))] hover:opacity-80"
|
|
1315
|
-
title="Clear focus"
|
|
1316
|
-
>
|
|
1317
|
-
<span
|
|
1318
|
-
className="inline-block h-2 w-2 rounded-full"
|
|
1319
|
-
style={{ backgroundColor: nodeColor(focusedNode.collection) }}
|
|
1320
|
-
/>
|
|
1321
|
-
Focused: {focusedNode.label}
|
|
1322
|
-
<span aria-hidden="true">×</span>
|
|
1323
|
-
</button>
|
|
1324
|
-
<label className="flex items-center gap-2 text-[rgb(var(--ec-page-text-muted))]">
|
|
1325
|
-
Depth
|
|
1326
|
-
<input
|
|
1327
|
-
type="range"
|
|
1328
|
-
min={1}
|
|
1329
|
-
max={3}
|
|
1330
|
-
step={1}
|
|
1331
|
-
value={focusDepth}
|
|
1332
|
-
onChange={(event) => setFocusDepth(Number(event.target.value))}
|
|
1333
|
-
className="w-24 accent-[rgb(var(--ec-accent))]"
|
|
1334
|
-
/>
|
|
1335
|
-
<span className="w-3 text-[rgb(var(--ec-page-text))]">{focusDepth}</span>
|
|
1336
|
-
</label>
|
|
1337
|
-
</>
|
|
1338
|
-
)}
|
|
1339
|
-
</div>
|
|
1422
|
+
)}
|
|
1340
1423
|
{focusedNode && (
|
|
1341
1424
|
<div className="absolute bottom-3 left-3 z-10 rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] p-3 text-xs shadow-xs">
|
|
1342
1425
|
<div className="mb-2 font-semibold text-[rgb(var(--ec-page-text))]">You are here</div>
|
|
@@ -1377,28 +1460,30 @@ const CatalogForceGraph = ({ nodes, links, linkLabels }: Props) => {
|
|
|
1377
1460
|
</ul>
|
|
1378
1461
|
</div>
|
|
1379
1462
|
)}
|
|
1380
|
-
|
|
1381
|
-
<div className="
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
<
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1463
|
+
{showLegend && (
|
|
1464
|
+
<div className="absolute bottom-3 right-3 z-10 rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] p-3 text-xs shadow-xs">
|
|
1465
|
+
<div className="mb-2 font-semibold text-[rgb(var(--ec-page-text))]">Resources</div>
|
|
1466
|
+
<ul className="space-y-1">
|
|
1467
|
+
{collectionsInGraph.map(({ collection, count }) => {
|
|
1468
|
+
const hidden = hiddenCollections.has(collection);
|
|
1469
|
+
return (
|
|
1470
|
+
<li key={collection}>
|
|
1471
|
+
<button
|
|
1472
|
+
type="button"
|
|
1473
|
+
onClick={() => toggleCollection(collection)}
|
|
1474
|
+
className={`flex w-full items-center gap-2 rounded-sm px-1 py-0.5 text-left hover:bg-[rgb(var(--ec-accent-subtle))] ${hidden ? 'opacity-40' : ''}`}
|
|
1475
|
+
>
|
|
1476
|
+
<span className="inline-block h-2.5 w-2.5 rounded-full" style={{ backgroundColor: nodeColor(collection) }} />
|
|
1477
|
+
<span className="text-[rgb(var(--ec-page-text))]">
|
|
1478
|
+
{COLLECTION_LABELS[collection] ?? collection} ({count})
|
|
1479
|
+
</span>
|
|
1480
|
+
</button>
|
|
1481
|
+
</li>
|
|
1482
|
+
);
|
|
1483
|
+
})}
|
|
1484
|
+
</ul>
|
|
1485
|
+
</div>
|
|
1486
|
+
)}
|
|
1402
1487
|
</div>
|
|
1403
1488
|
);
|
|
1404
1489
|
};
|
|
@@ -224,11 +224,6 @@ function FieldNodeGraphInner({
|
|
|
224
224
|
const { fitView } = useReactFlow();
|
|
225
225
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
226
226
|
|
|
227
|
-
// Load visualiser styles (animations, hover effects, theme variables)
|
|
228
|
-
useEffect(() => {
|
|
229
|
-
import('@eventcatalog/visualiser/styles-core.css');
|
|
230
|
-
}, []);
|
|
231
|
-
|
|
232
227
|
useEffect(() => {
|
|
233
228
|
const timer = setTimeout(() => fitView({ padding: 0.2 }), 50);
|
|
234
229
|
return () => clearTimeout(timer);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
import AstroArchitectureGraph from './AstroArchitectureGraph';
|
|
3
|
+
import { getCatalogForceGraph, toCatalogForceGraphWire } from '@utils/node-graphs/catalog-force-graph';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
// Resource to focus the graph on (its neighbourhood fills the view). Omit both
|
|
7
|
+
// to render the whole catalog graph.
|
|
8
|
+
id?: string;
|
|
9
|
+
collection?: string;
|
|
10
|
+
/** Relationship hops rendered around the focused resource (1–3) */
|
|
11
|
+
depth?: number;
|
|
12
|
+
/** Lens to render: all | domains | systems | services | teams | messages */
|
|
13
|
+
lens?: string;
|
|
14
|
+
showSearch?: boolean;
|
|
15
|
+
showLegend?: boolean;
|
|
16
|
+
/** Off by default — embedded graphs pin a lens rather than offer the picker */
|
|
17
|
+
showLensPicker?: boolean;
|
|
18
|
+
// Override the DOM portal the graph mounts into. Defaults to
|
|
19
|
+
// `${id}-architecture-graph-portal` (or `catalog-architecture-graph-portal`).
|
|
20
|
+
portalId?: string;
|
|
21
|
+
href?: {
|
|
22
|
+
label: string;
|
|
23
|
+
url: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
id,
|
|
29
|
+
collection,
|
|
30
|
+
depth = 2,
|
|
31
|
+
lens,
|
|
32
|
+
showSearch = true,
|
|
33
|
+
showLegend = true,
|
|
34
|
+
showLensPicker = false,
|
|
35
|
+
portalId,
|
|
36
|
+
href,
|
|
37
|
+
} = Astro.props;
|
|
38
|
+
|
|
39
|
+
const graph = await getCatalogForceGraph();
|
|
40
|
+
const wire = toCatalogForceGraphWire(graph);
|
|
41
|
+
|
|
42
|
+
// Only focus a resource that exists in the graph — it holds latest versions
|
|
43
|
+
// only, so e.g. an old-version page falls back to the whole catalog view
|
|
44
|
+
const focusKey =
|
|
45
|
+
id && collection && graph.nodes.some((node) => node.id === `${collection}/${id}`) ? `${collection}/${id}` : undefined;
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
<AstroArchitectureGraph
|
|
49
|
+
client:only="react"
|
|
50
|
+
portalId={portalId ?? `${id ?? 'catalog'}-architecture-graph-portal`}
|
|
51
|
+
nodes={wire.nodes}
|
|
52
|
+
links={wire.links}
|
|
53
|
+
linkLabels={wire.linkLabels}
|
|
54
|
+
initialFocus={focusKey}
|
|
55
|
+
initialFocusDepth={depth}
|
|
56
|
+
initialLens={lens}
|
|
57
|
+
showSearch={showSearch}
|
|
58
|
+
showLegend={showLegend}
|
|
59
|
+
showLensPicker={showLensPicker}
|
|
60
|
+
href={href?.url}
|
|
61
|
+
hrefLabel={href?.label}
|
|
62
|
+
/>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// The portal id pairs each scanner-rendered island with its MDX placeholder.
|
|
2
|
+
// The occurrence index keeps multiple no-id embeds on one page from colliding
|
|
3
|
+
// (the first occurrence keeps the unsuffixed id).
|
|
4
|
+
export const architectureGraphPortalId = (id: string | undefined, occurrenceIndex: number) =>
|
|
5
|
+
`${id ?? 'catalog'}-architecture-graph-portal${occurrenceIndex > 0 ? `-${occurrenceIndex}` : ''}`;
|
|
6
|
+
|
|
7
|
+
// Empty placeholder rendered by the MDX component map. The real graph is
|
|
8
|
+
// rendered by ArchitectureGraph.astro (which builds the data server-side) and
|
|
9
|
+
// portals itself into this div by id — same pattern as NodeGraphPortal.
|
|
10
|
+
const ArchitectureGraphPortal = (props: any) => {
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
className="not-prose h-[30em] my-6 mb-12 w-full relative border! border-[rgb(var(--ec-page-border))]! rounded-md overflow-hidden"
|
|
14
|
+
id={architectureGraphPortalId(props.id, props.occurrenceIndex ?? 0)}
|
|
15
|
+
style={{
|
|
16
|
+
maxHeight: props.maxHeight ? `${props.maxHeight}em` : `30em`,
|
|
17
|
+
}}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default ArchitectureGraphPortal;
|