@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.
- 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/catalog-to-astro-content-directory.cjs +1 -1
- package/dist/{chunk-I3U7AKYR.js → chunk-4GQNT666.js} +1 -1
- package/dist/{chunk-33AANWUG.js → chunk-6MDKVXWS.js} +1 -1
- package/dist/{chunk-6OL426GP.js → chunk-P3OQJLPP.js} +1 -1
- package/dist/{chunk-53JVQT34.js → chunk-QE6IMXND.js} +1 -1
- package/dist/{chunk-BX3YBDRJ.js → chunk-VGZG4TL6.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +66 -4
- package/dist/eventcatalog.config.d.cts +2 -1
- package/dist/eventcatalog.config.d.ts +2 -1
- package/dist/eventcatalog.js +73 -10
- 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/integrations/eventcatalog-features.ts +9 -0
- package/eventcatalog/src/components/Header.astro +5 -5
- package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +14 -11
- package/eventcatalog/src/components/MDX/NodeGraph/NodeGraphPortal.tsx +1 -1
- package/eventcatalog/src/components/MDX/RemoteFile.astro +5 -16
- package/eventcatalog/src/components/Search/Search.astro +1 -1
- package/eventcatalog/src/components/Search/SearchDataLoader.astro +25 -13
- package/eventcatalog/src/components/Search/SearchModal.tsx +101 -39
- package/eventcatalog/src/components/ThemeToggle.tsx +11 -3
- package/eventcatalog/src/content.config.ts +1 -0
- package/eventcatalog/src/enterprise/api/catalog.ts +22 -0
- package/eventcatalog/src/pages/api/search-index.json.ts +46 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +51 -5
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/_OpenAPI.tsx +26 -21
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/_index.data.ts +4 -4
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/index.astro +7 -2
- package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro +7 -2
- package/eventcatalog/src/pages/visualiser/[type]/[id]/index.astro +5 -0
- package/eventcatalog/src/pages/visualiser/context-map/index.astro +2 -2
- package/eventcatalog/src/pages/visualiser/designs/[id]/_index.data.ts +4 -4
- package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +6 -2
- package/eventcatalog/src/pages/visualiser/domain-integrations/index.astro +2 -2
- package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro +7 -2
- package/eventcatalog/src/utils/collections/util.ts +2 -0
- package/eventcatalog/src/utils/eventcatalog-config/catalog.ts +3 -18
- package/eventcatalog/src/utils/feature.ts +2 -0
- package/eventcatalog/src/utils/remote-file.ts +15 -0
- package/eventcatalog/src/utils/remote-spec.ts +45 -0
- package/package.json +5 -5
- 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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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,
|
|
203
|
+
id: url,
|
|
147
204
|
name: node.title,
|
|
148
|
-
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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 =
|
|
259
|
-
|
|
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
|
|
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,
|
|
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
|
-
{
|
|
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={
|
|
13
|
-
title={
|
|
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
|
}
|
|
@@ -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
|
-
|
|
36
|
-
|
|
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
|
|
49
|
+
// AsyncAPI parser will parse schemas for users, they can turn this off.
|
|
40
50
|
const parseSchemas = Config?.asyncAPI?.renderParsedSchemas ?? true;
|
|
41
|
-
const
|
|
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
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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="
|
|
23
|
-
<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="
|
|
24
|
-
<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="
|
|
8
|
+
<div class="m-4">
|
|
9
9
|
<div
|
|
10
|
-
class="h-[calc(100vh-130px)] w-full relative border border-
|
|
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="
|
|
21
|
-
<div
|
|
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}
|