@eventcatalog/core 3.13.0 → 3.14.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.
Files changed (50) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/catalog-to-astro-content-directory.cjs +1 -1
  6. package/dist/{chunk-I3U7AKYR.js → chunk-4GQNT666.js} +1 -1
  7. package/dist/{chunk-33AANWUG.js → chunk-6MDKVXWS.js} +1 -1
  8. package/dist/{chunk-6OL426GP.js → chunk-P3OQJLPP.js} +1 -1
  9. package/dist/{chunk-53JVQT34.js → chunk-QE6IMXND.js} +1 -1
  10. package/dist/{chunk-BX3YBDRJ.js → chunk-VGZG4TL6.js} +1 -1
  11. package/dist/constants.cjs +1 -1
  12. package/dist/constants.js +1 -1
  13. package/dist/eventcatalog.cjs +66 -4
  14. package/dist/eventcatalog.config.d.cts +2 -1
  15. package/dist/eventcatalog.config.d.ts +2 -1
  16. package/dist/eventcatalog.js +73 -10
  17. package/dist/generate.cjs +1 -1
  18. package/dist/generate.js +3 -3
  19. package/dist/utils/cli-logger.cjs +1 -1
  20. package/dist/utils/cli-logger.js +2 -2
  21. package/eventcatalog/integrations/eventcatalog-features.ts +9 -0
  22. package/eventcatalog/src/components/Header.astro +5 -5
  23. package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +14 -11
  24. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraphPortal.tsx +1 -1
  25. package/eventcatalog/src/components/MDX/RemoteFile.astro +5 -16
  26. package/eventcatalog/src/components/Search/Search.astro +1 -1
  27. package/eventcatalog/src/components/Search/SearchDataLoader.astro +25 -13
  28. package/eventcatalog/src/components/Search/SearchModal.tsx +101 -39
  29. package/eventcatalog/src/components/ThemeToggle.tsx +11 -3
  30. package/eventcatalog/src/content.config.ts +1 -0
  31. package/eventcatalog/src/enterprise/api/catalog.ts +22 -0
  32. package/eventcatalog/src/pages/api/search-index.json.ts +46 -0
  33. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +51 -5
  34. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/_OpenAPI.tsx +26 -21
  35. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/_index.data.ts +4 -4
  36. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/index.astro +7 -2
  37. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro +7 -2
  38. package/eventcatalog/src/pages/visualiser/[type]/[id]/index.astro +5 -0
  39. package/eventcatalog/src/pages/visualiser/context-map/index.astro +2 -2
  40. package/eventcatalog/src/pages/visualiser/designs/[id]/_index.data.ts +4 -4
  41. package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +6 -2
  42. package/eventcatalog/src/pages/visualiser/domain-integrations/index.astro +2 -2
  43. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro +7 -2
  44. package/eventcatalog/src/utils/collections/util.ts +2 -0
  45. package/eventcatalog/src/utils/eventcatalog-config/catalog.ts +3 -18
  46. package/eventcatalog/src/utils/feature.ts +2 -0
  47. package/eventcatalog/src/utils/remote-file.ts +15 -0
  48. package/eventcatalog/src/utils/remote-spec.ts +45 -0
  49. package/package.json +5 -5
  50. package/eventcatalog/src/pages/api/catalog.ts +0 -34
@@ -20,18 +20,30 @@
20
20
  if (!sidebarStore.get()) {
21
21
  const apiUrl = buildUrl('/api/sidebar-data.json', true);
22
22
 
23
- fetch(apiUrl)
24
- .then((response) => {
25
- if (!response.ok) {
26
- throw new Error(`Failed to fetch sidebar data: ${response.status}`);
27
- }
28
- return response.json();
29
- })
30
- .then((data) => {
31
- setSidebarData(data);
32
- })
33
- .catch((error) => {
34
- console.error('Error loading sidebar data:', error);
35
- });
23
+ const loadSidebarData = () => {
24
+ // Double-check cache in case another component loaded data while waiting for idle
25
+ if (sidebarStore.get()) return;
26
+
27
+ fetch(apiUrl)
28
+ .then((response) => {
29
+ if (!response.ok) {
30
+ throw new Error(`Failed to fetch sidebar data: ${response.status}`);
31
+ }
32
+ return response.json();
33
+ })
34
+ .then((data) => {
35
+ setSidebarData(data);
36
+ })
37
+ .catch((error) => {
38
+ console.error('Error loading sidebar data:', error);
39
+ });
40
+ };
41
+
42
+ // Defer non-critical sidebar payload fetch until browser is idle
43
+ if (typeof window.requestIdleCallback === 'function') {
44
+ window.requestIdleCallback(loadSidebarData, { timeout: 1200 });
45
+ } else {
46
+ setTimeout(loadSidebarData, 300);
47
+ }
36
48
  }
37
49
  </script>
@@ -24,8 +24,6 @@ import {
24
24
  } from '@heroicons/react/24/outline';
25
25
  import { StarIcon as StarIconSolid, CircleStackIcon } from '@heroicons/react/24/solid';
26
26
  import { useStore } from '@nanostores/react';
27
- import { sidebarStore } from '../../stores/sidebar-store';
28
- import type { NavNode } from '../../stores/sidebar-store/state';
29
27
  import { favoritesStore, toggleFavorite as toggleFavoriteAction } from '../../stores/favorites-store';
30
28
  import { buildUrl } from '@utils/url-builder';
31
29
 
@@ -99,11 +97,48 @@ const getUrlForItem = (node: any, key: string) => {
99
97
  return buildUrl(`/docs/${pluralType}/${id}/${version}`);
100
98
  };
101
99
 
100
+ interface SearchNode {
101
+ key: string;
102
+ title: string;
103
+ badge?: string;
104
+ summary?: string;
105
+ href?: string;
106
+ }
107
+
108
+ interface SearchNodeCompact {
109
+ k: string;
110
+ t: string;
111
+ b?: string;
112
+ s?: string;
113
+ h?: string;
114
+ }
115
+
116
+ interface SearchIndexPayload {
117
+ i?: SearchNodeCompact[];
118
+ items?: SearchNode[];
119
+ }
120
+
121
+ const normalizeSearchIndexPayload = (payload: SearchIndexPayload): SearchNode[] => {
122
+ if (payload.i) {
123
+ return payload.i.map((item) => ({
124
+ key: item.k,
125
+ title: item.t,
126
+ badge: item.b,
127
+ summary: item.s,
128
+ href: item.h,
129
+ }));
130
+ }
131
+
132
+ return payload.items || [];
133
+ };
134
+
102
135
  export default function SearchModal() {
103
136
  const [query, setQuery] = useState('');
104
137
  const [open, setOpen] = useState(false);
105
138
  const [activeFilter, setActiveFilter] = useState('all');
106
- const data = useStore(sidebarStore);
139
+ const [searchNodes, setSearchNodes] = useState<SearchNode[]>([]);
140
+ const [isLoadingSearchIndex, setIsLoadingSearchIndex] = useState(false);
141
+ const [searchIndexLoadError, setSearchIndexLoadError] = useState<string | null>(null);
107
142
  const favorites = useStore(favoritesStore);
108
143
  const inputRef = useRef<HTMLInputElement>(null);
109
144
 
@@ -122,6 +157,34 @@ export default function SearchModal() {
122
157
  return () => window.removeEventListener('searchModalToggle', handleModalToggle as EventListener);
123
158
  }, []);
124
159
 
160
+ useEffect(() => {
161
+ if (!open || searchNodes.length > 0 || isLoadingSearchIndex) {
162
+ return;
163
+ }
164
+
165
+ setIsLoadingSearchIndex(true);
166
+ setSearchIndexLoadError(null);
167
+
168
+ const apiUrl = buildUrl('/api/search-index.json', true);
169
+
170
+ fetch(apiUrl)
171
+ .then((response) => {
172
+ if (!response.ok) {
173
+ throw new Error(`Failed to fetch search index: ${response.status}`);
174
+ }
175
+ return response.json() as Promise<SearchIndexPayload>;
176
+ })
177
+ .then((payload) => {
178
+ setSearchNodes(normalizeSearchIndexPayload(payload));
179
+ })
180
+ .catch((error) => {
181
+ setSearchIndexLoadError(error instanceof Error ? error.message : 'Unable to load search index');
182
+ })
183
+ .finally(() => {
184
+ setIsLoadingSearchIndex(false);
185
+ });
186
+ }, [open, searchNodes.length, isLoadingSearchIndex]);
187
+
125
188
  const closeModal = () => {
126
189
  if ((window as any).searchModalState) {
127
190
  (window as any).searchModalState.close();
@@ -131,30 +194,22 @@ export default function SearchModal() {
131
194
  };
132
195
 
133
196
  const items = useMemo(() => {
134
- if (!data?.nodes) return [];
135
-
136
- // Extract all items from nodes
137
- const allItems = Object.entries(data.nodes)
138
- .map(([key, node]) => {
139
- // Skip reference entries (string values that point to other keys)
140
- if (typeof node === 'string') return null;
141
-
142
- const url = getUrlForItem(node, key);
197
+ return searchNodes
198
+ .map((node) => {
199
+ const url = getUrlForItem(node as any, node.key);
143
200
  if (!url) return null;
144
201
 
145
202
  return {
146
- id: url, // Use URL as unique ID
203
+ id: url,
147
204
  name: node.title,
148
- url: url,
205
+ url,
149
206
  type: node.badge || 'Page',
150
- key: key,
207
+ key: node.key,
151
208
  rawNode: node,
152
209
  };
153
210
  })
154
211
  .filter((item): item is NonNullable<typeof item> => item !== null);
155
-
156
- return allItems;
157
- }, [data]);
212
+ }, [searchNodes]);
158
213
 
159
214
  // Get searchable items (items that match the query but not filtered by type yet)
160
215
  const searchableItems = useMemo(() => {
@@ -236,17 +291,9 @@ export default function SearchModal() {
236
291
  });
237
292
  };
238
293
 
239
- // Helper to resolve a node, following references if needed
240
- const resolveNode = (key: string) => {
241
- if (!data?.nodes) return null;
242
- const node = data.nodes[key];
243
- if (!node) return null;
244
- // If it's a string reference, follow it
245
- if (typeof node === 'string') {
246
- return data.nodes[node] as NavNode | undefined;
247
- }
248
- return node;
249
- };
294
+ const searchNodeLookup = useMemo(() => {
295
+ return new Map(searchNodes.map((node) => [node.key, node]));
296
+ }, [searchNodes]);
250
297
 
251
298
  const filteredItems = useMemo(() => {
252
299
  if (query === '') {
@@ -255,18 +302,17 @@ export default function SearchModal() {
255
302
  return favorites
256
303
  .slice(0, 5)
257
304
  .map((fav) => {
258
- const node = resolveNode(fav.nodeKey);
259
- if (!node || typeof node === 'string') return null;
260
- const url = getUrlForItem(node, fav.nodeKey);
305
+ const node = searchNodeLookup.get(fav.nodeKey);
306
+ const url = node ? getUrlForItem(node as any, fav.nodeKey) : fav.href;
261
307
  if (!url) return null;
262
308
 
263
309
  return {
264
310
  id: url,
265
311
  name: fav.title,
266
- url: url,
267
- type: fav.badge || 'Page',
312
+ url,
313
+ type: fav.badge || node?.badge || 'Page',
268
314
  key: fav.nodeKey,
269
- rawNode: node,
315
+ rawNode: node || { title: fav.title, badge: fav.badge, summary: undefined },
270
316
  isFavorite: true,
271
317
  };
272
318
  })
@@ -290,7 +336,7 @@ export default function SearchModal() {
290
336
  }
291
337
 
292
338
  return result.slice(0, 50); // Limit results for performance
293
- }, [searchableItems, query, activeFilter, favorites, data]);
339
+ }, [searchableItems, query, activeFilter, favorites, searchNodeLookup]);
294
340
 
295
341
  return (
296
342
  <Transition.Root
@@ -368,7 +414,23 @@ export default function SearchModal() {
368
414
  ))}
369
415
  </div>
370
416
 
371
- {filteredItems.length > 0 && (
417
+ {isLoadingSearchIndex && (
418
+ <div className="py-10 px-6 text-center text-sm sm:px-14">
419
+ <MagnifyingGlassIcon className="mx-auto h-6 w-6 text-[rgb(var(--ec-icon-color))] animate-pulse" />
420
+ <p className="mt-4 font-semibold text-[rgb(var(--ec-page-text))]">Loading search index…</p>
421
+ <p className="mt-2 text-[rgb(var(--ec-page-text-muted))]">Preparing resources for search.</p>
422
+ </div>
423
+ )}
424
+
425
+ {searchIndexLoadError && !isLoadingSearchIndex && (
426
+ <div className="py-10 px-6 text-center text-sm sm:px-14">
427
+ <ExclamationCircleIcon className="mx-auto h-6 w-6 text-red-500" />
428
+ <p className="mt-4 font-semibold text-[rgb(var(--ec-page-text))]">Search unavailable</p>
429
+ <p className="mt-2 text-[rgb(var(--ec-page-text-muted))]">{searchIndexLoadError}</p>
430
+ </div>
431
+ )}
432
+
433
+ {!isLoadingSearchIndex && !searchIndexLoadError && filteredItems.length > 0 && (
372
434
  <>
373
435
  {query === '' && favorites.length > 0 && (
374
436
  <div className="px-6 pt-3 pb-2">
@@ -462,7 +524,7 @@ export default function SearchModal() {
462
524
  </>
463
525
  )}
464
526
 
465
- {query !== '' && filteredItems.length === 0 && (
527
+ {!isLoadingSearchIndex && !searchIndexLoadError && query !== '' && filteredItems.length === 0 && (
466
528
  <div className="py-14 px-6 text-center text-sm sm:px-14">
467
529
  <ExclamationCircleIcon
468
530
  type="outline"
@@ -476,7 +538,7 @@ export default function SearchModal() {
476
538
  </div>
477
539
  )}
478
540
 
479
- {query === '' && filteredItems.length === 0 && (
541
+ {!isLoadingSearchIndex && !searchIndexLoadError && query === '' && filteredItems.length === 0 && (
480
542
  <div className="py-14 px-6 text-center text-sm sm:px-14">
481
543
  <MagnifyingGlassIcon className="mx-auto h-6 w-6 text-[rgb(var(--ec-icon-color))]" />
482
544
  <p className="mt-4 font-semibold text-[rgb(var(--ec-page-text))]">Search for anything</p>
@@ -1,18 +1,26 @@
1
1
  import { useStore } from '@nanostores/react';
2
+ import { useEffect, useState } from 'react';
2
3
  import { Moon, Sun } from 'lucide-react';
3
4
  import { themeStore, toggleTheme } from '@stores/theme-store';
4
5
 
5
6
  export default function ThemeToggle() {
6
7
  const theme = useStore(themeStore);
8
+ const [mounted, setMounted] = useState(false);
9
+
10
+ useEffect(() => {
11
+ setMounted(true);
12
+ }, []);
13
+
14
+ const label = mounted ? `Switch to ${theme === 'light' ? 'dark' : 'light'} mode` : 'Toggle theme';
7
15
 
8
16
  return (
9
17
  <button
10
18
  onClick={toggleTheme}
11
19
  className="p-2 rounded-md text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-icon-hover))] hover:bg-[rgb(var(--ec-dropdown-hover))] transition-colors"
12
- aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
13
- title={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
20
+ aria-label={label}
21
+ title={label}
14
22
  >
15
- {theme === 'light' ? <Moon className="w-5 h-5" /> : <Sun className="w-5 h-5" />}
23
+ {!mounted ? <Moon className="w-5 h-5" /> : theme === 'light' ? <Moon className="w-5 h-5" /> : <Sun className="w-5 h-5" />}
16
24
  </button>
17
25
  );
18
26
  }
@@ -147,6 +147,7 @@ const baseSchema = z.object({
147
147
  type: z.enum(['openapi', 'asyncapi', 'graphql']),
148
148
  path: z.string(),
149
149
  name: z.string().optional(),
150
+ headers: z.record(z.string()).optional(),
150
151
  })
151
152
  ),
152
153
  ])
@@ -0,0 +1,22 @@
1
+ import type { APIRoute } from 'astro';
2
+ import utils from '@eventcatalog/sdk';
3
+ import { isSSR } from '@utils/feature';
4
+
5
+ /**
6
+ * Route that dumps the whole catalog as JSON (without markdown)
7
+ * Experimental API
8
+ *
9
+ * This route is injected only when `api.fullCatalogAPIEnabled` is true.
10
+ */
11
+ export const GET: APIRoute = async () => {
12
+ const { dumpCatalog } = utils(process.env.PROJECT_DIR || '');
13
+ const catalog = await dumpCatalog({ includeMarkdown: false });
14
+
15
+ return new Response(JSON.stringify(catalog), {
16
+ headers: {
17
+ 'Content-Type': 'application/json',
18
+ },
19
+ });
20
+ };
21
+
22
+ export const prerender = !isSSR();
@@ -0,0 +1,46 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { getNestedSideBarData } from '@stores/sidebar-store/state';
3
+
4
+ const isDev = import.meta.env.DEV;
5
+
6
+ interface SearchIndexItemCompact {
7
+ k: string;
8
+ t: string;
9
+ b?: string;
10
+ s?: string;
11
+ h?: string;
12
+ }
13
+
14
+ const SKIPPED_PREFIXES = ['list:'];
15
+
16
+ export const GET: APIRoute = async () => {
17
+ const sidebarData = await getNestedSideBarData();
18
+
19
+ const items = Object.entries(sidebarData.nodes).reduce<SearchIndexItemCompact[]>((acc, [key, node]) => {
20
+ if (typeof node === 'string') return acc;
21
+ if (SKIPPED_PREFIXES.some((prefix) => key.startsWith(prefix))) return acc;
22
+ if (!node.title) return acc;
23
+
24
+ acc.push({
25
+ k: key,
26
+ t: node.title,
27
+ ...(node.badge ? { b: node.badge } : {}),
28
+ ...(node.summary ? { s: node.summary } : {}),
29
+ ...(node.href ? { h: node.href } : {}),
30
+ });
31
+
32
+ return acc;
33
+ }, []);
34
+
35
+ return new Response(JSON.stringify({ i: items }), {
36
+ headers: {
37
+ 'Content-Type': 'application/json',
38
+ 'Cache-Control': isDev
39
+ ? 'no-cache, no-store, must-revalidate'
40
+ : 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400',
41
+ Vary: 'Accept-Encoding',
42
+ },
43
+ });
44
+ };
45
+
46
+ export const prerender = !isDev;
@@ -5,6 +5,8 @@ import { Parser } from '@asyncapi/parser';
5
5
  import { AvroSchemaParser } from '@asyncapi/avro-schema-parser';
6
6
  import fs from 'fs';
7
7
 
8
+ import { isSameOrigin, resolveHeaders } from '@utils/remote-spec';
9
+
8
10
  import '@asyncapi/react-component/styles/default.min.css';
9
11
  import js from '@asyncapi/react-component/browser/standalone/without-parser.js?url';
10
12
  import { AsyncApiComponentWP, type ConfigInterface } from '@asyncapi/react-component';
@@ -19,7 +21,7 @@ export const prerender = Page.prerender;
19
21
  export const getStaticPaths = Page.getStaticPaths;
20
22
 
21
23
  // Get data
22
- const { collection, data, filePath, filename, path: relativeSpecPath } = await Page.getData(Astro);
24
+ const { collection, data, filePath, filename, path: relativeSpecPath, headers = {} } = await Page.getData(Astro);
23
25
 
24
26
  const fileName = filename || 'asyncapi.yaml';
25
27
  const pathToSpec = getAbsoluteFilePathForAstroFile(filePath, fileName);
@@ -27,18 +29,62 @@ const fileExists = fs.existsSync(pathToSpec);
27
29
  const isRemote = relativeSpecPath.includes('https://');
28
30
  let content = '';
29
31
 
32
+ const allowAnyEnvInSpecHeaders = Config?.asyncAPI?.allowAnyEnvInSpecHeaders ?? false;
33
+ const resolvedHeaders = resolveHeaders(headers, { allowAnyEnvInHeaders: allowAnyEnvInSpecHeaders });
34
+
30
35
  if (fileExists && !isRemote) {
31
36
  content = fs.readFileSync(pathToSpec, 'utf8');
32
37
  }
33
38
 
34
39
  if (isRemote) {
35
- // Fetch the content from the remote path
36
- content = await fetch(relativeSpecPath).then((res) => res.text());
40
+ const response = await fetch(relativeSpecPath, { headers: resolvedHeaders });
41
+
42
+ if (!response.ok) {
43
+ throw new Error(`Failed to fetch AsyncAPI spec: ${response.status} ${response.statusText}`);
44
+ }
45
+
46
+ content = await response.text();
37
47
  }
38
48
 
39
- // AsyncAPI parser will parser schemas for users, they can turn this off.
49
+ // AsyncAPI parser will parse schemas for users, they can turn this off.
40
50
  const parseSchemas = Config?.asyncAPI?.renderParsedSchemas ?? true;
41
- const parsed = await new Parser({ schemaParsers: [AvroSchemaParser()] }).parse(content, { parseSchemas });
51
+ const parserOptions: NonNullable<Parameters<Parser['parse']>[1]> = {
52
+ parseSchemas,
53
+ source: isRemote ? relativeSpecPath : pathToSpec,
54
+ };
55
+
56
+ if (isRemote && Object.keys(resolvedHeaders).length > 0) {
57
+ // NOTE: AsyncAPI parser exposes resolver wiring via __unstable.
58
+ // Keep this isolated so parser upgrades can adjust in one place.
59
+ const resolverWithHeaders = {
60
+ canRead: true,
61
+ read: async (uri: { toString: () => string }) => {
62
+ const targetUrl = uri.toString();
63
+ const shouldForwardHeaders = isSameOrigin(relativeSpecPath, targetUrl);
64
+
65
+ const response = await fetch(targetUrl, {
66
+ headers: shouldForwardHeaders ? resolvedHeaders : undefined,
67
+ });
68
+
69
+ if (!response.ok) {
70
+ throw new Error(`Failed to fetch reference: ${response.status} ${response.statusText}`);
71
+ }
72
+
73
+ return response.text();
74
+ },
75
+ };
76
+
77
+ parserOptions.__unstable = {
78
+ resolver: {
79
+ resolvers: [
80
+ { schema: 'https', ...resolverWithHeaders },
81
+ { schema: 'http', ...resolverWithHeaders },
82
+ ],
83
+ },
84
+ };
85
+ }
86
+
87
+ const parsed = await new Parser({ schemaParsers: [AvroSchemaParser()] }).parse(content, parserOptions);
42
88
  const stringified = parsed.document?.json();
43
89
  const config: ConfigInterface = { show: { sidebar: true, errors: true } };
44
90
 
@@ -1,8 +1,11 @@
1
- import { useState, useEffect } from 'react';
2
- import { ApiReferenceReact } from '@scalar/api-reference-react';
1
+ import { useState, useEffect, lazy, Suspense } from 'react';
3
2
  import '@scalar/api-reference-react/style.css';
4
3
  import './_styles.css';
5
4
 
5
+ const ApiReferenceReact = lazy(() =>
6
+ import('@scalar/api-reference-react').then((module) => ({ default: module.ApiReferenceReact }))
7
+ );
8
+
6
9
  const OpenAPISpec = ({ spec }: { spec: string }) => {
7
10
  const [loaded, setLoaded] = useState(false);
8
11
  const [isDarkMode, setIsDarkMode] = useState(() => {
@@ -31,25 +34,27 @@ const OpenAPISpec = ({ spec }: { spec: string }) => {
31
34
  return (
32
35
  <div>
33
36
  {!loaded && <div>Loading...</div>}
34
- <ApiReferenceReact
35
- key={isDarkMode ? 'dark' : 'light'}
36
- configuration={{
37
- spec: {
38
- content: spec,
39
- },
40
- theme: 'fastify',
41
- hideClientButton: true,
42
- onLoaded: () => {
43
- setLoaded(true);
44
- },
45
- forceDarkModeState: isDarkMode ? 'dark' : 'light',
46
- darkMode: isDarkMode,
47
- defaultOpenAllTags: true,
48
- hideDarkModeToggle: true,
49
- searchHotKey: 'p',
50
- showSidebar: true,
51
- }}
52
- />
37
+ <Suspense fallback={<div>Loading OpenAPI reference...</div>}>
38
+ <ApiReferenceReact
39
+ key={isDarkMode ? 'dark' : 'light'}
40
+ configuration={{
41
+ spec: {
42
+ content: spec,
43
+ },
44
+ theme: 'fastify',
45
+ hideClientButton: true,
46
+ onLoaded: () => {
47
+ setLoaded(true);
48
+ },
49
+ forceDarkModeState: isDarkMode ? 'dark' : 'light',
50
+ darkMode: isDarkMode,
51
+ defaultOpenAllTags: true,
52
+ hideDarkModeToggle: true,
53
+ searchHotKey: 'p',
54
+ showSidebar: true,
55
+ }}
56
+ />
57
+ </Suspense>
53
58
  </div>
54
59
  );
55
60
  };
@@ -1,5 +1,5 @@
1
1
  import { HybridPage } from '@utils/page-loaders/hybrid-page';
2
- import { isAuthEnabled } from '@utils/feature';
2
+ import { isAuthEnabled, isVisualiserEnabled } from '@utils/feature';
3
3
  import { getServices, type Service } from '@utils/collections/services';
4
4
 
5
5
  const serviceHasData = (service: Service) => {
@@ -8,7 +8,7 @@ const serviceHasData = (service: Service) => {
8
8
 
9
9
  export class Page extends HybridPage {
10
10
  static async getStaticPaths(): Promise<Array<{ params: any; props: any }>> {
11
- if (isAuthEnabled()) {
11
+ if (isAuthEnabled() || !isVisualiserEnabled()) {
12
12
  return [];
13
13
  }
14
14
 
@@ -33,7 +33,7 @@ export class Page extends HybridPage {
33
33
  protected static async fetchData(params: any) {
34
34
  const { id, version } = params;
35
35
 
36
- if (!id || !version) {
36
+ if (!id || !version || !isVisualiserEnabled()) {
37
37
  return null;
38
38
  }
39
39
 
@@ -58,7 +58,7 @@ export class Page extends HybridPage {
58
58
  }
59
59
 
60
60
  static get clientAuthScript(): string {
61
- if (!isAuthEnabled()) {
61
+ if (!isAuthEnabled() || !isVisualiserEnabled()) {
62
62
  return '';
63
63
  }
64
64
 
@@ -19,8 +19,13 @@ const {
19
19
  ---
20
20
 
21
21
  <VisualiserLayout title={`Visualiser | ${props.data.name} (${props.collection})`} description={props.data.summary}>
22
- <div class="bg-gray-100/50 m-4">
23
- <div class="h-[calc(100vh-130px)] w-full relative border border-gray-200" id={`${id}-portal`} transition:animate="fade"></div>
22
+ <div class="m-4">
23
+ <div
24
+ class="h-[calc(100vh-130px)] w-full relative border border-[rgb(var(--ec-page-border))] rounded-md"
25
+ id={`${id}-portal`}
26
+ transition:animate="fade"
27
+ >
28
+ </div>
24
29
  <NodeGraph
25
30
  id={id}
26
31
  collection={`${collection}-containers`}
@@ -20,8 +20,13 @@ const {
20
20
  ---
21
21
 
22
22
  <VisualiserLayout title={`Visualiser | ${props.data.name} (${props.collection})`} description={props.data.summary}>
23
- <div class="bg-gray-100/50 m-4">
24
- <div class="h-[calc(100vh-130px)] w-full relative border border-gray-200" id={`${id}-portal`} transition:animate="fade"></div>
23
+ <div class="m-4">
24
+ <div
25
+ class="h-[calc(100vh-130px)] w-full relative border border-[rgb(var(--ec-page-border))] rounded-md"
26
+ id={`${id}-portal`}
27
+ transition:animate="fade"
28
+ >
29
+ </div>
25
30
  <NodeGraph
26
31
  id={id}
27
32
  collection={collection}
@@ -8,8 +8,13 @@ import { getDomains } from '@utils/collections/domains';
8
8
  import { getContainers } from '@utils/collections/containers';
9
9
  import type { CollectionEntry } from 'astro:content';
10
10
  import type { CollectionTypes } from '@types';
11
+ import { isVisualiserEnabled } from '@utils/feature';
11
12
 
12
13
  export async function getStaticPaths() {
14
+ if (!isVisualiserEnabled()) {
15
+ return [];
16
+ }
17
+
13
18
  const [events, commands, services, domains, containers] = await Promise.all([
14
19
  getEvents(),
15
20
  getCommands(),
@@ -5,9 +5,9 @@ import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
5
5
  ---
6
6
 
7
7
  <VerticalSideBarLayout title={`Visualiser | Bounded context map`}>
8
- <div class="bg-gray-100/50 m-4">
8
+ <div class="m-4">
9
9
  <div
10
- class="h-[calc(100vh-130px)] w-full relative border border-gray-200"
10
+ class="h-[calc(100vh-130px)] w-full relative border border-[rgb(var(--ec-page-border))] rounded-md"
11
11
  id={`domain-context-map-portal`}
12
12
  transition:animate="fade"
13
13
  >
@@ -1,9 +1,9 @@
1
1
  import { HybridPage } from '@utils/page-loaders/hybrid-page';
2
- import { isAuthEnabled } from '@utils/feature';
2
+ import { isAuthEnabled, isVisualiserEnabled } from '@utils/feature';
3
3
 
4
4
  export class Page extends HybridPage {
5
5
  static async getStaticPaths(): Promise<Array<{ params: any; props: any }>> {
6
- if (isAuthEnabled()) {
6
+ if (isAuthEnabled() || !isVisualiserEnabled()) {
7
7
  return [];
8
8
  }
9
9
 
@@ -26,7 +26,7 @@ export class Page extends HybridPage {
26
26
  protected static async fetchData(params: any) {
27
27
  const { id } = params;
28
28
 
29
- if (!id) {
29
+ if (!id || !isVisualiserEnabled()) {
30
30
  return null;
31
31
  }
32
32
 
@@ -53,7 +53,7 @@ export class Page extends HybridPage {
53
53
  }
54
54
 
55
55
  static get clientAuthScript(): string {
56
- if (!isAuthEnabled()) {
56
+ if (!isAuthEnabled() || !isVisualiserEnabled()) {
57
57
  return '';
58
58
  }
59
59
 
@@ -17,8 +17,12 @@ const { data } = props;
17
17
  ---
18
18
 
19
19
  <VerticalSideBarLayout title={`Visualiser | Bounded context map`}>
20
- <div class="bg-gray-100/50 m-4">
21
- <div class="h-[calc(100vh-130px)] w-full relative border border-gray-200" id={`${data.id}-portal`} transition:animate="fade">
20
+ <div class="m-4">
21
+ <div
22
+ class="h-[calc(100vh-130px)] w-full relative border border-[rgb(var(--ec-page-border))] rounded-md"
23
+ id={`${data.id}-portal`}
24
+ transition:animate="fade"
25
+ >
22
26
  </div>
23
27
  <AstroNodeGraph
24
28
  id={data.id}