@eventcatalog/core 3.8.2 → 3.9.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/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-OCD75GFW.js → chunk-CZG5RQXY.js} +1 -1
- package/dist/{chunk-I7HRERRK.js → chunk-FM44RPBS.js} +1 -1
- package/dist/{chunk-MOBOWLEW.js → chunk-HJOMVPCL.js} +1 -1
- package/dist/{chunk-275AT7XV.js → chunk-OQYLI4YJ.js} +1 -1
- package/dist/{chunk-KBMXUUXX.js → chunk-UYBPI4UO.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/src/components/ChatPanel/ChatPanel.tsx +50 -0
- package/eventcatalog/src/components/MDX/Design/Design.astro +4 -1
- package/eventcatalog/src/components/MDX/EntityMap/EntityMap.astro +4 -0
- package/eventcatalog/src/components/MDX/Flow/Flow.astro +4 -1
- package/eventcatalog/src/components/MDX/NodeGraph/MermaidView.tsx +240 -0
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +4 -0
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.tsx +333 -189
- package/eventcatalog/src/components/MDX/NodeGraph/VisualizerDropdownContent.tsx +224 -0
- package/eventcatalog/src/content.config.ts +1 -1
- package/eventcatalog/src/enterprise/ai/chat-api.ts +23 -0
- package/eventcatalog/src/enterprise/tools/catalog-tools.ts +96 -0
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version].mermaid.ts +128 -0
- package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +4 -0
- package/eventcatalog/src/utils/clipboard.ts +22 -0
- package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +299 -0
- package/package.json +1 -1
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import React, { type RefObject } from 'react';
|
|
2
|
+
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
|
3
|
+
import { Code, Share2, Search, Grid3x3, Maximize2, Map, Sparkles, Zap, EyeOff, ExternalLink } from 'lucide-react';
|
|
4
|
+
import { DocumentArrowDownIcon, PresentationChartLineIcon } from '@heroicons/react/24/outline';
|
|
5
|
+
import type { VisualiserSearchRef } from './VisualiserSearch';
|
|
6
|
+
|
|
7
|
+
interface VisualizerDropdownContentProps {
|
|
8
|
+
isMermaidView: boolean;
|
|
9
|
+
setIsMermaidView: (value: boolean) => void;
|
|
10
|
+
animateMessages: boolean;
|
|
11
|
+
toggleAnimateMessages: () => void;
|
|
12
|
+
hideChannels: boolean;
|
|
13
|
+
toggleChannelsVisibility: () => void;
|
|
14
|
+
hasChannels: boolean;
|
|
15
|
+
showMinimap: boolean;
|
|
16
|
+
setShowMinimap: (value: boolean) => void;
|
|
17
|
+
handleFitView: () => void;
|
|
18
|
+
searchRef: RefObject<VisualiserSearchRef | null>;
|
|
19
|
+
isChatEnabled: boolean;
|
|
20
|
+
openChat: () => void;
|
|
21
|
+
handleCopyArchitectureCode: () => void;
|
|
22
|
+
handleExportVisual: () => void;
|
|
23
|
+
setIsShareModalOpen: (value: boolean) => void;
|
|
24
|
+
toggleFullScreen: () => void;
|
|
25
|
+
openStudioModal: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const VisualizerDropdownContent: React.FC<VisualizerDropdownContentProps> = ({
|
|
29
|
+
isMermaidView,
|
|
30
|
+
setIsMermaidView,
|
|
31
|
+
animateMessages,
|
|
32
|
+
toggleAnimateMessages,
|
|
33
|
+
hideChannels,
|
|
34
|
+
toggleChannelsVisibility,
|
|
35
|
+
hasChannels,
|
|
36
|
+
showMinimap,
|
|
37
|
+
setShowMinimap,
|
|
38
|
+
handleFitView,
|
|
39
|
+
searchRef,
|
|
40
|
+
isChatEnabled,
|
|
41
|
+
openChat,
|
|
42
|
+
handleCopyArchitectureCode,
|
|
43
|
+
handleExportVisual,
|
|
44
|
+
setIsShareModalOpen,
|
|
45
|
+
toggleFullScreen,
|
|
46
|
+
openStudioModal,
|
|
47
|
+
}) => {
|
|
48
|
+
return (
|
|
49
|
+
<>
|
|
50
|
+
{/* Canvas Settings Submenu */}
|
|
51
|
+
<DropdownMenu.Sub>
|
|
52
|
+
<DropdownMenu.SubTrigger className="flex items-center px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer transition-colors gap-2 outline-none">
|
|
53
|
+
<Grid3x3 className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
|
|
54
|
+
<span className="flex-1 font-normal">Canvas</span>
|
|
55
|
+
<svg className="w-3 h-3 text-[rgb(var(--ec-page-text-muted))]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
56
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
57
|
+
</svg>
|
|
58
|
+
</DropdownMenu.SubTrigger>
|
|
59
|
+
<DropdownMenu.Portal>
|
|
60
|
+
<DropdownMenu.SubContent
|
|
61
|
+
className="min-w-[200px] bg-[rgb(var(--ec-card-bg))] rounded-lg shadow-xl border border-[rgb(var(--ec-page-border))] py-1.5 z-[60]"
|
|
62
|
+
sideOffset={8}
|
|
63
|
+
alignOffset={-8}
|
|
64
|
+
>
|
|
65
|
+
<DropdownMenu.CheckboxItem
|
|
66
|
+
checked={isMermaidView}
|
|
67
|
+
onCheckedChange={setIsMermaidView}
|
|
68
|
+
className="flex items-center px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer transition-colors gap-2"
|
|
69
|
+
>
|
|
70
|
+
<Code className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
|
|
71
|
+
<span className="flex-1 font-normal">Render as mermaid</span>
|
|
72
|
+
<div
|
|
73
|
+
className={`w-7 h-4 rounded-full transition-all duration-200 flex-shrink-0 relative ${isMermaidView ? 'bg-[rgb(var(--ec-accent))]' : 'bg-[rgb(var(--ec-page-border))]'}`}
|
|
74
|
+
>
|
|
75
|
+
<div
|
|
76
|
+
className={`absolute top-0.5 w-3 h-3 rounded-full bg-white shadow-sm transition-all duration-200 ${isMermaidView ? 'left-3.5' : 'left-0.5'}`}
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
</DropdownMenu.CheckboxItem>
|
|
80
|
+
|
|
81
|
+
<DropdownMenu.Separator className="my-1 h-px bg-[rgb(var(--ec-page-border))]" />
|
|
82
|
+
|
|
83
|
+
<DropdownMenu.CheckboxItem
|
|
84
|
+
checked={animateMessages}
|
|
85
|
+
onCheckedChange={toggleAnimateMessages}
|
|
86
|
+
className="flex items-center px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer transition-colors gap-2"
|
|
87
|
+
>
|
|
88
|
+
<Zap className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
|
|
89
|
+
<span className="flex-1 font-normal">Simulate Messages</span>
|
|
90
|
+
<div
|
|
91
|
+
className={`w-7 h-4 rounded-full transition-all duration-200 flex-shrink-0 relative ${animateMessages ? 'bg-[rgb(var(--ec-accent))]' : 'bg-[rgb(var(--ec-page-border))]'}`}
|
|
92
|
+
>
|
|
93
|
+
<div
|
|
94
|
+
className={`absolute top-0.5 w-3 h-3 rounded-full bg-white shadow-sm transition-all duration-200 ${animateMessages ? 'left-3.5' : 'left-0.5'}`}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
</DropdownMenu.CheckboxItem>
|
|
98
|
+
|
|
99
|
+
{hasChannels && (
|
|
100
|
+
<DropdownMenu.CheckboxItem
|
|
101
|
+
checked={hideChannels}
|
|
102
|
+
onCheckedChange={toggleChannelsVisibility}
|
|
103
|
+
className="flex items-center px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer transition-colors gap-2"
|
|
104
|
+
>
|
|
105
|
+
<EyeOff className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
|
|
106
|
+
<span className="flex-1 font-normal">Hide channels</span>
|
|
107
|
+
<div
|
|
108
|
+
className={`w-7 h-4 rounded-full transition-all duration-200 flex-shrink-0 relative ${hideChannels ? 'bg-[rgb(var(--ec-accent))]' : 'bg-[rgb(var(--ec-page-border))]'}`}
|
|
109
|
+
>
|
|
110
|
+
<div
|
|
111
|
+
className={`absolute top-0.5 w-3 h-3 rounded-full bg-white shadow-sm transition-all duration-200 ${hideChannels ? 'left-3.5' : 'left-0.5'}`}
|
|
112
|
+
/>
|
|
113
|
+
</div>
|
|
114
|
+
</DropdownMenu.CheckboxItem>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
<DropdownMenu.CheckboxItem
|
|
118
|
+
checked={showMinimap}
|
|
119
|
+
onCheckedChange={setShowMinimap}
|
|
120
|
+
className="flex items-center px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer transition-colors gap-2"
|
|
121
|
+
>
|
|
122
|
+
<Map className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
|
|
123
|
+
<span className="flex-1 font-normal">Show minimap</span>
|
|
124
|
+
<div
|
|
125
|
+
className={`w-7 h-4 rounded-full transition-all duration-200 flex-shrink-0 relative ${showMinimap ? 'bg-[rgb(var(--ec-accent))]' : 'bg-[rgb(var(--ec-page-border))]'}`}
|
|
126
|
+
>
|
|
127
|
+
<div
|
|
128
|
+
className={`absolute top-0.5 w-3 h-3 rounded-full bg-white shadow-sm transition-all duration-200 ${showMinimap ? 'left-3.5' : 'left-0.5'}`}
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
</DropdownMenu.CheckboxItem>
|
|
132
|
+
|
|
133
|
+
<DropdownMenu.Separator className="my-1 h-px bg-[rgb(var(--ec-page-border))]" />
|
|
134
|
+
|
|
135
|
+
<DropdownMenu.Item
|
|
136
|
+
onClick={handleFitView}
|
|
137
|
+
className="px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors"
|
|
138
|
+
>
|
|
139
|
+
<Maximize2 className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
|
|
140
|
+
<span className="flex-1 font-normal">Fit to view</span>
|
|
141
|
+
</DropdownMenu.Item>
|
|
142
|
+
|
|
143
|
+
<DropdownMenu.Item
|
|
144
|
+
onClick={() => {
|
|
145
|
+
searchRef.current?.hideSuggestions();
|
|
146
|
+
setTimeout(() => {
|
|
147
|
+
const searchInput = document.querySelector('input[placeholder="Search nodes..."]') as HTMLInputElement;
|
|
148
|
+
searchInput?.focus();
|
|
149
|
+
}, 50);
|
|
150
|
+
}}
|
|
151
|
+
className="px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors"
|
|
152
|
+
>
|
|
153
|
+
<Search className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
|
|
154
|
+
<span className="flex-1 font-normal">Find on canvas</span>
|
|
155
|
+
</DropdownMenu.Item>
|
|
156
|
+
</DropdownMenu.SubContent>
|
|
157
|
+
</DropdownMenu.Portal>
|
|
158
|
+
</DropdownMenu.Sub>
|
|
159
|
+
|
|
160
|
+
{/* Ask AI */}
|
|
161
|
+
{isChatEnabled && (
|
|
162
|
+
<>
|
|
163
|
+
<DropdownMenu.Separator className="my-1 h-px bg-[rgb(var(--ec-page-border))]" />
|
|
164
|
+
<DropdownMenu.Item
|
|
165
|
+
onClick={openChat}
|
|
166
|
+
className="px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors"
|
|
167
|
+
>
|
|
168
|
+
<Sparkles className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
|
|
169
|
+
<span className="flex-1 font-normal">Ask a question</span>
|
|
170
|
+
</DropdownMenu.Item>
|
|
171
|
+
</>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{/* Export Items */}
|
|
175
|
+
<DropdownMenu.Separator className="my-1 h-px bg-[rgb(var(--ec-page-border))]" />
|
|
176
|
+
<DropdownMenu.Item
|
|
177
|
+
onClick={handleCopyArchitectureCode}
|
|
178
|
+
className="px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors"
|
|
179
|
+
>
|
|
180
|
+
<Code className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
|
|
181
|
+
<span className="flex-1 font-normal">Copy as mermaid</span>
|
|
182
|
+
</DropdownMenu.Item>
|
|
183
|
+
|
|
184
|
+
<DropdownMenu.Item
|
|
185
|
+
onClick={handleExportVisual}
|
|
186
|
+
className="px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors"
|
|
187
|
+
>
|
|
188
|
+
<DocumentArrowDownIcon className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
|
|
189
|
+
<span className="flex-1 font-normal">Export image</span>
|
|
190
|
+
</DropdownMenu.Item>
|
|
191
|
+
|
|
192
|
+
{/* Share Link */}
|
|
193
|
+
<DropdownMenu.Item
|
|
194
|
+
onClick={() => setIsShareModalOpen(true)}
|
|
195
|
+
className="px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors"
|
|
196
|
+
>
|
|
197
|
+
<Share2 className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
|
|
198
|
+
<span className="flex-1 font-normal">Share Link</span>
|
|
199
|
+
</DropdownMenu.Item>
|
|
200
|
+
|
|
201
|
+
{/* Start Presentation */}
|
|
202
|
+
<DropdownMenu.Separator className="my-1 h-px bg-[rgb(var(--ec-page-border))]" />
|
|
203
|
+
<DropdownMenu.Item
|
|
204
|
+
onClick={toggleFullScreen}
|
|
205
|
+
className="px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors"
|
|
206
|
+
>
|
|
207
|
+
<PresentationChartLineIcon className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
|
|
208
|
+
<span className="flex-1 font-normal">Start Presentation</span>
|
|
209
|
+
</DropdownMenu.Item>
|
|
210
|
+
|
|
211
|
+
{/* Open in EventCatalog Studio */}
|
|
212
|
+
<DropdownMenu.Separator className="my-1 h-px bg-[rgb(var(--ec-page-border))]" />
|
|
213
|
+
<DropdownMenu.Item
|
|
214
|
+
onClick={openStudioModal}
|
|
215
|
+
className="px-3 py-2 text-xs text-[rgb(var(--ec-page-text))] hover:bg-[rgb(var(--ec-accent-subtle)/0.3)] cursor-pointer flex items-center gap-2 transition-colors"
|
|
216
|
+
>
|
|
217
|
+
<ExternalLink className="w-3.5 h-3.5 text-[rgb(var(--ec-page-text-muted))] flex-shrink-0" />
|
|
218
|
+
<span className="flex-1 font-normal">Open in EventCatalog Studio</span>
|
|
219
|
+
</DropdownMenu.Item>
|
|
220
|
+
</>
|
|
221
|
+
);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
export default VisualizerDropdownContent;
|
|
@@ -453,7 +453,7 @@ const dataClassificationEnum = z.enum(['public', 'internal', 'confidential', 're
|
|
|
453
453
|
|
|
454
454
|
const containers = defineCollection({
|
|
455
455
|
loader: glob({
|
|
456
|
-
pattern: ['**/containers
|
|
456
|
+
pattern: ['**/containers/**/index.(md|mdx)', '**/containers/**/versioned/*/index.(md|mdx)'],
|
|
457
457
|
base: projectDirBase,
|
|
458
458
|
generateId: ({ data }) => {
|
|
459
459
|
return `${data.id}-${data.version}`;
|
|
@@ -11,9 +11,11 @@ import {
|
|
|
11
11
|
getSchemaForResource as getSchemaImpl,
|
|
12
12
|
getDataProductInputs as getDataProductInputsImpl,
|
|
13
13
|
getDataProductOutputs as getDataProductOutputsImpl,
|
|
14
|
+
getArchitectureDiagramAsMermaid as getArchitectureDiagramImpl,
|
|
14
15
|
collectionSchema,
|
|
15
16
|
resourceCollectionSchema,
|
|
16
17
|
messageCollectionSchema,
|
|
18
|
+
visualiserCollectionSchema,
|
|
17
19
|
toolDescriptions,
|
|
18
20
|
} from '@enterprise/tools/catalog-tools';
|
|
19
21
|
|
|
@@ -61,6 +63,7 @@ const builtInToolsMetadata = [
|
|
|
61
63
|
{ name: 'getSchemaForResource', description: 'Get the schema or specifications (OpenAPI, AsyncAPI, GraphQL) for a resource' },
|
|
62
64
|
{ name: 'getDataProductInputs', description: 'Get the inputs (resources consumed) for a data product' },
|
|
63
65
|
{ name: 'getDataProductOutputs', description: 'Get the outputs (resources produced) for a data product with data contracts' },
|
|
66
|
+
{ name: 'getArchitectureDiagramAsMermaid', description: 'Get the architecture diagram for a resource as Mermaid code' },
|
|
64
67
|
];
|
|
65
68
|
|
|
66
69
|
// Get extended tools metadata from user configuration
|
|
@@ -112,6 +115,11 @@ You may be able to get the resource from the URL
|
|
|
112
115
|
|
|
113
116
|
The referer URL is: ${referrer}
|
|
114
117
|
|
|
118
|
+
IMPORTANT: When the user is on a visualiser page (/visualiser/*) or asks about architecture, dependencies, data flow, or connections:
|
|
119
|
+
- Use the getArchitectureDiagramAsMermaid tool to fetch the architecture diagram
|
|
120
|
+
- The mermaid code shows all the connections between resources (services, events, channels, etc.)
|
|
121
|
+
- Use this to provide accurate, contextual answers about the architecture
|
|
122
|
+
|
|
115
123
|
Sometimes the user will be on the specification page (openapi, asyncapi, graphql) for a resource too
|
|
116
124
|
- /docs/services/OrdersService/0.0.3/asyncapi/order-service-asyncapi -> id: OrdersService, version: 0.0.3, collection: services, schema (specification): asyncapi
|
|
117
125
|
- /docs/services/OrdersService/0.0.3/spec/openapi-v2 -> id: OrdersService, version: 0.0.3, collection: services, schema (specification): openapi
|
|
@@ -317,6 +325,21 @@ export const POST = async ({ request }: APIContext<{ question: string; messages:
|
|
|
317
325
|
return await getDataProductOutputsImpl({ dataProductId, dataProductVersion });
|
|
318
326
|
},
|
|
319
327
|
}),
|
|
328
|
+
getArchitectureDiagramAsMermaid: tool({
|
|
329
|
+
description: toolDescriptions.getArchitectureDiagramAsMermaid,
|
|
330
|
+
inputSchema: z.object({
|
|
331
|
+
resourceId: z.string().describe('The id of the resource to get the architecture diagram for'),
|
|
332
|
+
resourceVersion: z.string().describe('The version of the resource to get the architecture diagram for'),
|
|
333
|
+
resourceCollection: visualiserCollectionSchema
|
|
334
|
+
.describe(
|
|
335
|
+
'The collection of the resource (events, commands, queries, services, domains, flows, containers, data-products)'
|
|
336
|
+
)
|
|
337
|
+
.default('services'),
|
|
338
|
+
}),
|
|
339
|
+
execute: async ({ resourceId, resourceVersion, resourceCollection }) => {
|
|
340
|
+
return await getArchitectureDiagramImpl({ resourceId, resourceVersion, resourceCollection });
|
|
341
|
+
},
|
|
342
|
+
}),
|
|
320
343
|
suggestFollowUpQuestions: tool({
|
|
321
344
|
description:
|
|
322
345
|
'Use this tool after answering a question to suggest 2-3 relevant follow-up questions the user might want to ask. These will be displayed as clickable suggestions.',
|
|
@@ -9,6 +9,18 @@ import { getItemsFromCollectionByIdAndSemverOrLatest } from '@utils/collections/
|
|
|
9
9
|
import { getUbiquitousLanguageWithSubdomains } from '@utils/collections/domains';
|
|
10
10
|
import { getAbsoluteFilePathForAstroFile } from '@utils/files';
|
|
11
11
|
import fs from 'node:fs';
|
|
12
|
+
import { getNodesAndEdges as getNodesAndEdgesForService } from '@utils/node-graphs/services-node-graph';
|
|
13
|
+
import {
|
|
14
|
+
getNodesAndEdgesForCommands,
|
|
15
|
+
getNodesAndEdgesForEvents,
|
|
16
|
+
getNodesAndEdgesForQueries,
|
|
17
|
+
} from '@utils/node-graphs/message-node-graph';
|
|
18
|
+
import { getNodesAndEdges as getNodesAndEdgesForDomain } from '@utils/node-graphs/domains-node-graph';
|
|
19
|
+
import { getNodesAndEdges as getNodesAndEdgesForFlows } from '@utils/node-graphs/flows-node-graph';
|
|
20
|
+
import { getNodesAndEdges as getNodesAndEdgesForDataProduct } from '@utils/node-graphs/data-products-node-graph';
|
|
21
|
+
import { getNodesAndEdges as getNodesAndEdgesForContainer } from '@utils/node-graphs/container-node-graph';
|
|
22
|
+
import { convertToMermaid } from '@utils/node-graphs/export-mermaid';
|
|
23
|
+
import config from '@config';
|
|
12
24
|
|
|
13
25
|
// ============================================
|
|
14
26
|
// Pagination utilities
|
|
@@ -110,6 +122,17 @@ export const resourceCollectionSchema = z.enum([
|
|
|
110
122
|
'data-products',
|
|
111
123
|
]);
|
|
112
124
|
|
|
125
|
+
export const visualiserCollectionSchema = z.enum([
|
|
126
|
+
'events',
|
|
127
|
+
'commands',
|
|
128
|
+
'queries',
|
|
129
|
+
'services',
|
|
130
|
+
'domains',
|
|
131
|
+
'flows',
|
|
132
|
+
'containers',
|
|
133
|
+
'data-products',
|
|
134
|
+
]);
|
|
135
|
+
|
|
113
136
|
// ============================================
|
|
114
137
|
// Tool implementations (core logic)
|
|
115
138
|
// ============================================
|
|
@@ -821,6 +844,77 @@ export async function getDataProductOutputs(params: { dataProductId: string; dat
|
|
|
821
844
|
};
|
|
822
845
|
}
|
|
823
846
|
|
|
847
|
+
// ============================================
|
|
848
|
+
// Architecture Diagram (Mermaid) tools
|
|
849
|
+
// ============================================
|
|
850
|
+
|
|
851
|
+
const getNodesAndEdgesFunctions = {
|
|
852
|
+
services: getNodesAndEdgesForService,
|
|
853
|
+
events: getNodesAndEdgesForEvents,
|
|
854
|
+
commands: getNodesAndEdgesForCommands,
|
|
855
|
+
queries: getNodesAndEdgesForQueries,
|
|
856
|
+
domains: getNodesAndEdgesForDomain,
|
|
857
|
+
flows: getNodesAndEdgesForFlows,
|
|
858
|
+
containers: getNodesAndEdgesForContainer,
|
|
859
|
+
'data-products': getNodesAndEdgesForDataProduct,
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* Get the architecture diagram for a resource as Mermaid code
|
|
864
|
+
* Returns flowchart syntax that can be rendered or used for understanding architecture
|
|
865
|
+
*/
|
|
866
|
+
export async function getArchitectureDiagramAsMermaid(params: {
|
|
867
|
+
resourceId: string;
|
|
868
|
+
resourceVersion: string;
|
|
869
|
+
resourceCollection: string;
|
|
870
|
+
}) {
|
|
871
|
+
const { resourceId, resourceVersion, resourceCollection } = params;
|
|
872
|
+
|
|
873
|
+
// Validate the collection is supported for visualisation
|
|
874
|
+
if (!(resourceCollection in getNodesAndEdgesFunctions)) {
|
|
875
|
+
return {
|
|
876
|
+
error: `Collection '${resourceCollection}' does not support architecture diagrams. Supported collections: ${Object.keys(getNodesAndEdgesFunctions).join(', ')}`,
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
try {
|
|
881
|
+
// Get nodes and edges for this resource
|
|
882
|
+
const { nodes, edges } = await getNodesAndEdgesFunctions[resourceCollection as keyof typeof getNodesAndEdgesFunctions]({
|
|
883
|
+
id: resourceId,
|
|
884
|
+
version: resourceVersion,
|
|
885
|
+
mode: 'full',
|
|
886
|
+
channelRenderMode: config.visualiser?.channels?.renderMode === 'single' ? 'single' : 'flat',
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
if (!nodes || nodes.length === 0) {
|
|
890
|
+
return {
|
|
891
|
+
error: `No diagram data available for ${resourceCollection}/${resourceId} (v${resourceVersion})`,
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Convert to mermaid
|
|
896
|
+
const mermaidCode = convertToMermaid(nodes, edges, {
|
|
897
|
+
includeStyles: true,
|
|
898
|
+
direction: 'LR',
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
return {
|
|
902
|
+
resourceId,
|
|
903
|
+
resourceVersion,
|
|
904
|
+
resourceCollection,
|
|
905
|
+
mermaidCode,
|
|
906
|
+
nodeCount: nodes.length,
|
|
907
|
+
edgeCount: edges.length,
|
|
908
|
+
visualiserUrl: `/visualiser/${resourceCollection}/${resourceId}/${resourceVersion}`,
|
|
909
|
+
};
|
|
910
|
+
} catch (error) {
|
|
911
|
+
console.error('Error generating mermaid diagram:', error);
|
|
912
|
+
return {
|
|
913
|
+
error: `Failed to generate diagram for ${resourceCollection}/${resourceId} (v${resourceVersion})`,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
824
918
|
// ============================================
|
|
825
919
|
// Tool metadata (descriptions)
|
|
826
920
|
// ============================================
|
|
@@ -854,4 +948,6 @@ export const toolDescriptions = {
|
|
|
854
948
|
'Use this tool to get the inputs (resources consumed) for a data product. Returns fully hydrated input resources with their id, version, name, summary, and collection type.',
|
|
855
949
|
getDataProductOutputs:
|
|
856
950
|
'Use this tool to get the outputs (resources produced) for a data product. Returns fully hydrated output resources with their id, version, name, summary, collection type, and data contracts (if defined). Data contracts include the contract name, path, format, type, and content.',
|
|
951
|
+
getArchitectureDiagramAsMermaid:
|
|
952
|
+
'Use this tool to get the architecture diagram for a resource as Mermaid flowchart code. This shows how the resource connects to other resources (services, events, channels, etc.) in the architecture. The mermaid code can be rendered to visualize the architecture or used to understand relationships. Supported collections: events, commands, queries, services, domains, flows, containers, data-products.',
|
|
857
953
|
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API endpoint to return mermaid diagram code for a visualiser page
|
|
3
|
+
* URL: /visualiser/{type}/{id}/{version}.mermaid
|
|
4
|
+
*
|
|
5
|
+
* Returns plain text mermaid flowchart syntax that can be used by LLMs,
|
|
6
|
+
* documentation tools, or pasted into mermaid-compatible renderers.
|
|
7
|
+
*/
|
|
8
|
+
import type { APIRoute, GetStaticPaths } from 'astro';
|
|
9
|
+
import { isAuthEnabled, isVisualiserEnabled } from '@utils/feature';
|
|
10
|
+
import { pageDataLoader } from '@utils/page-loaders/page-data-loader';
|
|
11
|
+
import { getNodesAndEdges as getNodesAndEdgesForService } from '@utils/node-graphs/services-node-graph';
|
|
12
|
+
import {
|
|
13
|
+
getNodesAndEdgesForCommands,
|
|
14
|
+
getNodesAndEdgesForEvents,
|
|
15
|
+
getNodesAndEdgesForQueries,
|
|
16
|
+
} from '@utils/node-graphs/message-node-graph';
|
|
17
|
+
import { getNodesAndEdges as getNodesAndEdgesForDomain } from '@utils/node-graphs/domains-node-graph';
|
|
18
|
+
import { getNodesAndEdges as getNodesAndEdgesForFlows } from '@utils/node-graphs/flows-node-graph';
|
|
19
|
+
import { getNodesAndEdges as getNodesAndEdgesForDataProduct } from '@utils/node-graphs/data-products-node-graph';
|
|
20
|
+
import { getNodesAndEdges as getNodesAndEdgesForContainer } from '@utils/node-graphs/container-node-graph';
|
|
21
|
+
import { convertToMermaid } from '@utils/node-graphs/export-mermaid';
|
|
22
|
+
import config from '@config';
|
|
23
|
+
import type { PageTypes } from '@types';
|
|
24
|
+
|
|
25
|
+
type PageTypesWithFlows = PageTypes | 'flows';
|
|
26
|
+
|
|
27
|
+
// Prerender static pages when auth is disabled, use SSR when auth is enabled
|
|
28
|
+
export const prerender = !isAuthEnabled();
|
|
29
|
+
|
|
30
|
+
const getNodesAndEdgesFunctions = {
|
|
31
|
+
services: getNodesAndEdgesForService,
|
|
32
|
+
events: getNodesAndEdgesForEvents,
|
|
33
|
+
commands: getNodesAndEdgesForCommands,
|
|
34
|
+
queries: getNodesAndEdgesForQueries,
|
|
35
|
+
domains: getNodesAndEdgesForDomain,
|
|
36
|
+
flows: getNodesAndEdgesForFlows,
|
|
37
|
+
containers: getNodesAndEdgesForContainer,
|
|
38
|
+
'data-products': getNodesAndEdgesForDataProduct,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const getStaticPaths: GetStaticPaths = async () => {
|
|
42
|
+
if (isAuthEnabled() || !isVisualiserEnabled()) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { getFlows } = await import('@utils/collections/flows');
|
|
47
|
+
|
|
48
|
+
const loaders = {
|
|
49
|
+
...pageDataLoader,
|
|
50
|
+
flows: getFlows,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const itemTypes: PageTypesWithFlows[] = [
|
|
54
|
+
'events',
|
|
55
|
+
'commands',
|
|
56
|
+
'queries',
|
|
57
|
+
'services',
|
|
58
|
+
'domains',
|
|
59
|
+
'flows',
|
|
60
|
+
'containers',
|
|
61
|
+
'data-products',
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const allItems = await Promise.all(itemTypes.map((type) => loaders[type]()));
|
|
65
|
+
|
|
66
|
+
return allItems.flatMap((items, index) =>
|
|
67
|
+
items
|
|
68
|
+
.filter((item) => item.data.visualiser !== false)
|
|
69
|
+
.map((item) => ({
|
|
70
|
+
params: {
|
|
71
|
+
type: itemTypes[index],
|
|
72
|
+
id: item.data.id,
|
|
73
|
+
version: item.data.version,
|
|
74
|
+
},
|
|
75
|
+
}))
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const GET: APIRoute = async ({ params }) => {
|
|
80
|
+
const { type, id, version } = params;
|
|
81
|
+
|
|
82
|
+
if (!type || !id || !version || !isVisualiserEnabled()) {
|
|
83
|
+
return new Response('Not found', { status: 404 });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Validate the type is supported
|
|
87
|
+
if (!(type in getNodesAndEdgesFunctions)) {
|
|
88
|
+
return new Response(`Unsupported type: ${type}`, { status: 400 });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
// Get nodes and edges for this resource
|
|
93
|
+
const { nodes, edges } = await getNodesAndEdgesFunctions[type as keyof typeof getNodesAndEdgesFunctions]({
|
|
94
|
+
id,
|
|
95
|
+
version,
|
|
96
|
+
mode: 'full',
|
|
97
|
+
channelRenderMode: config.visualiser?.channels?.renderMode === 'single' ? 'single' : 'flat',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
if (!nodes || nodes.length === 0) {
|
|
101
|
+
return new Response('No diagram data available', { status: 404 });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Convert to mermaid
|
|
105
|
+
const mermaidCode = convertToMermaid(nodes, edges, {
|
|
106
|
+
includeStyles: true,
|
|
107
|
+
direction: 'LR',
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Add header comment with metadata
|
|
111
|
+
const header = `%% EventCatalog Mermaid Diagram
|
|
112
|
+
%% Resource: ${type}/${id} (v${version})
|
|
113
|
+
%% Generated: ${new Date().toISOString()}
|
|
114
|
+
|
|
115
|
+
`;
|
|
116
|
+
|
|
117
|
+
return new Response(header + mermaidCode, {
|
|
118
|
+
status: 200,
|
|
119
|
+
headers: {
|
|
120
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
121
|
+
'Cache-Control': 'public, max-age=3600',
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error('Error generating mermaid diagram:', error);
|
|
126
|
+
return new Response('Failed to generate diagram', { status: 500 });
|
|
127
|
+
}
|
|
128
|
+
};
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
import NodeGraph from '@components/MDX/NodeGraph/NodeGraph';
|
|
3
3
|
import { ClientRouter } from 'astro:transitions';
|
|
4
4
|
import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
|
|
5
|
+
import { isEventCatalogChatEnabled } from '@utils/feature';
|
|
5
6
|
|
|
6
7
|
import { Page } from './_index.data';
|
|
7
8
|
|
|
9
|
+
const isChatEnabled = isEventCatalogChatEnabled();
|
|
10
|
+
|
|
8
11
|
export const prerender = Page.prerender;
|
|
9
12
|
export const getStaticPaths = Page.getStaticPaths;
|
|
10
13
|
|
|
@@ -25,6 +28,7 @@ const { data } = props;
|
|
|
25
28
|
client:only="react"
|
|
26
29
|
mode="full"
|
|
27
30
|
linkTo="visualiser"
|
|
31
|
+
isChatEnabled={isChatEnabled}
|
|
28
32
|
/>
|
|
29
33
|
</div>
|
|
30
34
|
<ClientRouter />
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copy text to clipboard with fallback for older browsers
|
|
3
|
+
* @param text - The text to copy to clipboard
|
|
4
|
+
* @returns Promise that resolves to true if successful
|
|
5
|
+
*/
|
|
6
|
+
export async function copyToClipboard(text: string): Promise<boolean> {
|
|
7
|
+
try {
|
|
8
|
+
await navigator.clipboard.writeText(text);
|
|
9
|
+
return true;
|
|
10
|
+
} catch {
|
|
11
|
+
// Fallback for older browsers
|
|
12
|
+
const textarea = document.createElement('textarea');
|
|
13
|
+
textarea.value = text;
|
|
14
|
+
textarea.style.position = 'fixed';
|
|
15
|
+
textarea.style.opacity = '0';
|
|
16
|
+
document.body.appendChild(textarea);
|
|
17
|
+
textarea.select();
|
|
18
|
+
document.execCommand('copy');
|
|
19
|
+
document.body.removeChild(textarea);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
}
|