@eventcatalog/core 2.57.1 → 2.58.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.
Files changed (31) 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/{chunk-6PT76NQN.js → chunk-GKUPYLJL.js} +1 -1
  6. package/dist/{chunk-76HGJTBT.js → chunk-PAKFTVIV.js} +1 -1
  7. package/dist/{chunk-7BNIEBLU.js → chunk-TIETK4WY.js} +1 -1
  8. package/dist/constants.cjs +1 -1
  9. package/dist/constants.js +1 -1
  10. package/dist/eventcatalog.cjs +1 -1
  11. package/dist/eventcatalog.js +3 -3
  12. package/eventcatalog/astro.config.mjs +6 -1
  13. package/eventcatalog/public/icons/graphql.svg +1 -0
  14. package/eventcatalog/src/components/Lists/PillListFlat.tsx +108 -27
  15. package/eventcatalog/src/components/Lists/SpecificationsList.astro +15 -0
  16. package/eventcatalog/src/components/MDX/Attachments.astro +158 -0
  17. package/eventcatalog/src/components/MDX/Tiles/Tile.astro +31 -15
  18. package/eventcatalog/src/components/MDX/Tiles/Tiles.astro +3 -3
  19. package/eventcatalog/src/components/MDX/components.tsx +2 -0
  20. package/eventcatalog/src/components/SideBars/ChannelSideBar.astro +25 -0
  21. package/eventcatalog/src/components/SideBars/DomainSideBar.astro +25 -0
  22. package/eventcatalog/src/components/SideBars/EntitySideBar.astro +24 -0
  23. package/eventcatalog/src/components/SideBars/FlowSideBar.astro +25 -0
  24. package/eventcatalog/src/components/SideBars/MessageSideBar.astro +25 -0
  25. package/eventcatalog/src/components/SideBars/ServiceSideBar.astro +25 -0
  26. package/eventcatalog/src/components/SideNav/ListViewSideBar/components/SpecificationList.tsx +20 -0
  27. package/eventcatalog/src/content.config.ts +20 -1
  28. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +177 -0
  29. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/_[filename].data.ts +98 -0
  30. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +43 -6
  31. package/package.json +2 -1
@@ -25,6 +25,8 @@ const subDomains = (domain.data.domains as CollectionEntry<'domains'>[]) || [];
25
25
  // @ts-ignore
26
26
  const entities = (domain.data.entities as CollectionEntry<'entities'>[]) || [];
27
27
 
28
+ const attachments = domain.data.attachments || [];
29
+
28
30
  const ubiquitousLanguage = await getUbiquitousLanguage(domain);
29
31
  const hasUbiquitousLanguage = ubiquitousLanguage.length > 0;
30
32
  const ubiquitousLanguageDictionary = hasUbiquitousLanguage ? ubiquitousLanguage[0].data.dictionary : [];
@@ -106,6 +108,18 @@ const ownersList = filteredOwners.map((o) => ({
106
108
  href: buildUrl(`/docs/${o.collection}/${o.data.id}`),
107
109
  }));
108
110
 
111
+ const attachmentsList = attachments.map((a) => {
112
+ const attachmentIsURL = typeof a === 'string';
113
+
114
+ return {
115
+ label: attachmentIsURL ? a : (a.title ?? a.url),
116
+ href: attachmentIsURL ? a : a.url,
117
+ icon: attachmentIsURL ? 'ExternalLinkIcon' : (a.icon ?? 'ExternalLinkIcon'),
118
+ target: '_blank' as const,
119
+ subgroup: attachmentIsURL ? undefined : (a.type ?? ''),
120
+ };
121
+ });
122
+
109
123
  const shouldRenderSideBarSection = (section: string) => {
110
124
  if (!domain.data.detailsPanel) {
111
125
  return true;
@@ -208,6 +222,17 @@ const shouldRenderSideBarSection = (section: string) => {
208
222
  <VersionList versions={domain.data.versions} collectionItem={domain} />
209
223
  )
210
224
  }
225
+ {
226
+ domain.data.attachments && shouldRenderSideBarSection('attachments') && (
227
+ <PillListFlat
228
+ title={`Attachments (${attachmentsList.length})`}
229
+ pills={attachmentsList}
230
+ emptyMessage={`This domain does not have any attachments.`}
231
+ color="pink"
232
+ client:load
233
+ />
234
+ )
235
+ }
211
236
  {
212
237
  domain.data.repository && shouldRenderSideBarSection('repository') && (
213
238
  <RepositoryList repository={domain.data.repository?.url} language={domain.data.repository?.language} />
@@ -23,6 +23,19 @@ const services = (entity.data.services as CollectionEntry<'services'>[]) || [];
23
23
  // @ts-ignore
24
24
  const domains = (entity.data.domains as CollectionEntry<'domains'>[]) || [];
25
25
 
26
+ const attachments = entity.data.attachments || [];
27
+
28
+ const attachmentsList = attachments.map((a) => {
29
+ const attachmentIsURL = typeof a === 'string';
30
+ return {
31
+ label: attachmentIsURL ? a : (a.title ?? a.url),
32
+ href: attachmentIsURL ? a : a.url,
33
+ icon: attachmentIsURL ? 'ExternalLinkIcon' : (a.icon ?? 'ExternalLinkIcon'),
34
+ target: '_blank' as const,
35
+ subgroup: attachmentIsURL ? undefined : (a.type ?? ''),
36
+ };
37
+ });
38
+
26
39
  const ownersList = filteredOwners.map((o) => ({
27
40
  label: o.data.name,
28
41
  type: o.collection,
@@ -87,6 +100,17 @@ const shouldRenderSideBarSection = (section: string) => {
87
100
  <VersionList versions={entity.data.versions} collectionItem={entity} />
88
101
  )
89
102
  }
103
+ {
104
+ entity.data.attachments && shouldRenderSideBarSection('attachments') && (
105
+ <PillListFlat
106
+ title={`Attachments (${attachmentsList.length})`}
107
+ pills={attachmentsList}
108
+ emptyMessage={`This entity does not have any attachments.`}
109
+ color="pink"
110
+ client:load
111
+ />
112
+ )
113
+ }
90
114
  {
91
115
  filteredOwners.length > 0 && shouldRenderSideBarSection('owners') && (
92
116
  <OwnersList
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import OwnersList from '@components/Lists/OwnersList';
3
3
  import VersionList from '@components/Lists/VersionList.astro';
4
+ import PillListFlat from '@components/Lists/PillListFlat';
4
5
  import { buildUrl } from '@utils/url-builder';
5
6
  import { getOwner } from '@utils/collections/owners';
6
7
  import type { CollectionEntry } from 'astro:content';
@@ -30,6 +31,19 @@ const ownersList = filteredOwners.map((o) => ({
30
31
 
31
32
  const isRSSEnabled = config.rss?.enabled;
32
33
 
34
+ const attachments = flow.data.attachments || [];
35
+
36
+ const attachmentsList = attachments.map((a) => {
37
+ const attachmentIsURL = typeof a === 'string';
38
+ return {
39
+ label: attachmentIsURL ? a : (a.title ?? a.url),
40
+ href: attachmentIsURL ? a : a.url,
41
+ icon: attachmentIsURL ? 'ExternalLinkIcon' : (a.icon ?? 'ExternalLinkIcon'),
42
+ target: '_blank' as const,
43
+ subgroup: attachmentIsURL ? undefined : (a.type ?? ''),
44
+ };
45
+ });
46
+
33
47
  const shouldRenderSideBarSection = (section: string) => {
34
48
  if (!flow.data.detailsPanel) {
35
49
  return true;
@@ -51,6 +65,17 @@ const shouldRenderSideBarSection = (section: string) => {
51
65
  <VersionList versions={flow.data.versions} collectionItem={flow} />
52
66
  )
53
67
  }
68
+ {
69
+ flow.data.attachments && shouldRenderSideBarSection('attachments') && (
70
+ <PillListFlat
71
+ title={`Attachments (${attachmentsList.length})`}
72
+ pills={attachmentsList}
73
+ emptyMessage={`This flow does not have any attachments.`}
74
+ color="pink"
75
+ client:load
76
+ />
77
+ )
78
+ }
54
79
 
55
80
  {
56
81
  shouldRenderSideBarSection('owners') && (
@@ -29,6 +29,19 @@ const filteredOwners = owners.filter((o) => o !== undefined);
29
29
 
30
30
  const resourceGroups = message.data?.resourceGroups || [];
31
31
 
32
+ const attachments = message.data.attachments || [];
33
+
34
+ const attachmentsList = attachments.map((a) => {
35
+ const attachmentIsURL = typeof a === 'string';
36
+ return {
37
+ label: attachmentIsURL ? a : (a.title ?? a.url),
38
+ href: attachmentIsURL ? a : a.url,
39
+ icon: attachmentIsURL ? 'ExternalLinkIcon' : (a.icon ?? 'ExternalLinkIcon'),
40
+ target: '_blank' as const,
41
+ subgroup: attachmentIsURL ? undefined : (a.type ?? ''),
42
+ };
43
+ });
44
+
32
45
  const producerList = producers.map((p) => ({
33
46
  label: `${p.data.name}`,
34
47
  tag: `v${p.data.version}`,
@@ -153,6 +166,18 @@ const shouldRenderSideBarSection = (section: string) => {
153
166
  )
154
167
  }
155
168
 
169
+ {
170
+ message.data.attachments && shouldRenderSideBarSection('attachments') && (
171
+ <PillListFlat
172
+ title={`Attachments (${attachmentsList.length})`}
173
+ pills={attachmentsList}
174
+ emptyMessage={`This ${type} does not have any attachments.`}
175
+ color="pink"
176
+ client:load
177
+ />
178
+ )
179
+ }
180
+
156
181
  {
157
182
  ownersList.length > 0 && shouldRenderSideBarSection('owners') && (
158
183
  <OwnersList
@@ -35,6 +35,19 @@ const resourceGroups = service.data?.resourceGroups || [];
35
35
 
36
36
  const domainsServiceBelongsTo = await getDomainsForService(service);
37
37
 
38
+ const attachments = service.data.attachments || [];
39
+
40
+ const attachmentsList = attachments.map((a) => {
41
+ const attachmentIsURL = typeof a === 'string';
42
+ return {
43
+ label: attachmentIsURL ? a : (a.title ?? a.url),
44
+ href: attachmentIsURL ? a : a.url,
45
+ icon: attachmentIsURL ? 'ExternalLinkIcon' : (a.icon ?? 'ExternalLinkIcon'),
46
+ target: '_blank' as const,
47
+ subgroup: attachmentIsURL ? undefined : (a.type ?? ''),
48
+ };
49
+ });
50
+
38
51
  const sendsList = sends
39
52
  .sort((a, b) => a.collection.localeCompare(b.collection))
40
53
  .map((p) => ({
@@ -141,6 +154,18 @@ const shouldRenderSideBarSection = (section: string) => {
141
154
  )
142
155
  }
143
156
 
157
+ {
158
+ service.data.attachments && shouldRenderSideBarSection('attachments') && (
159
+ <PillListFlat
160
+ title={`Attachments (${attachmentsList.length})`}
161
+ pills={attachmentsList}
162
+ emptyMessage={`This service does not have any attachments.`}
163
+ color="pink"
164
+ client:load
165
+ />
166
+ )
167
+ }
168
+
144
169
  {
145
170
  service.data.specifications && shouldRenderSideBarSection('specifications') && (
146
171
  <SpecificationsList collectionItem={service} />
@@ -17,6 +17,7 @@ interface SpecificationListProps {
17
17
  const SpecificationList: React.FC<SpecificationListProps> = ({ specifications, id, version }) => {
18
18
  const asyncAPISpecifications = specifications.filter((spec) => spec.type === 'asyncapi');
19
19
  const openAPISpecifications = specifications.filter((spec) => spec.type === 'openapi');
20
+ const graphQLSpecifications = specifications.filter((spec) => spec.type === 'graphql');
20
21
 
21
22
  return (
22
23
  <ul className="space-y-0.5 border-l border-gray-200/80 ml-[9px] pl-4">
@@ -56,6 +57,25 @@ const SpecificationList: React.FC<SpecificationListProps> = ({ specifications, i
56
57
  </span>
57
58
  </a>
58
59
  ))}
60
+ {graphQLSpecifications &&
61
+ graphQLSpecifications.length > 0 &&
62
+ graphQLSpecifications.map((spec) => (
63
+ <a
64
+ key={`${spec.name}-openapi`}
65
+ href={buildUrl(`/docs/services/${id}/${version}/graphql/${spec.filenameWithoutExtension}`)}
66
+ data-active={window.location.href.includes(`docs/services/${id}/${version}/graphql/${spec.filenameWithoutExtension}`)}
67
+ className={`items-center px-2 py-1.5 text-xs text-gray-600 hover:bg-purple-100 rounded-md flex justify-between ${
68
+ window.location.href.includes(`docs/services/${id}/${version}/graphql/${spec.filenameWithoutExtension}`)
69
+ ? 'bg-purple-100'
70
+ : 'hover:bg-purple-100'
71
+ }`}
72
+ >
73
+ <span className="truncate flex items-center gap-1">{spec.name}</span>
74
+ <span className="text-green-600 ml-2 text-[10px] uppercase font-medium bg-gray-50 px-4 py-0.5 rounded">
75
+ <img src={buildUrl('/icons/graphql.svg', true)} className="w-4 h-4" alt="GraphQL" />
76
+ </span>
77
+ </a>
78
+ ))}
59
79
  </ul>
60
80
  );
61
81
  };
@@ -103,10 +103,11 @@ const baseSchema = z.object({
103
103
  z.object({
104
104
  openapiPath: z.string().optional(),
105
105
  asyncapiPath: z.string().optional(),
106
+ graphqlPath: z.string().optional(),
106
107
  }),
107
108
  z.array(
108
109
  z.object({
109
- type: z.enum(['openapi', 'asyncapi']),
110
+ type: z.enum(['openapi', 'asyncapi', 'graphql']),
110
111
  path: z.string(),
111
112
  name: z.string().optional(),
112
113
  })
@@ -147,6 +148,20 @@ const baseSchema = z.object({
147
148
  ])
148
149
  .optional(),
149
150
  visualiser: z.boolean().optional(),
151
+ attachments: z
152
+ .array(
153
+ z.union([
154
+ z.string().url(), // simple case
155
+ z.object({
156
+ url: z.string().url(),
157
+ title: z.string().optional(),
158
+ type: z.string().optional(), // e.g. "architecture-record", "diagram"
159
+ description: z.string().optional(),
160
+ icon: z.string().optional(),
161
+ }),
162
+ ])
163
+ )
164
+ .optional(),
150
165
  // Used by eventcatalog
151
166
  versions: z.array(z.string()).optional(),
152
167
  latestVersion: z.string().optional(),
@@ -259,6 +274,7 @@ const messageDetailsPanelPropertySchema = z.object({
259
274
  repository: detailPanelPropertySchema.optional(),
260
275
  owners: detailPanelPropertySchema.optional(),
261
276
  changelog: detailPanelPropertySchema.optional(),
277
+ attachments: detailPanelPropertySchema.optional(),
262
278
  });
263
279
 
264
280
  const events = defineCollection({
@@ -411,6 +427,7 @@ const domains = defineCollection({
411
427
  versions: detailPanelPropertySchema.optional(),
412
428
  owners: detailPanelPropertySchema.optional(),
413
429
  changelog: detailPanelPropertySchema.optional(),
430
+ attachments: detailPanelPropertySchema.optional(),
414
431
  })
415
432
  .optional(),
416
433
  })
@@ -451,6 +468,7 @@ const channels = defineCollection({
451
468
  repository: detailPanelPropertySchema.optional(),
452
469
  owners: detailPanelPropertySchema.optional(),
453
470
  changelog: detailPanelPropertySchema.optional(),
471
+ attachments: detailPanelPropertySchema.optional(),
454
472
  })
455
473
  .optional(),
456
474
  })
@@ -522,6 +540,7 @@ const entities = defineCollection({
522
540
  versions: detailPanelPropertySchema.optional(),
523
541
  owners: detailPanelPropertySchema.optional(),
524
542
  changelog: detailPanelPropertySchema.optional(),
543
+ attachments: detailPanelPropertySchema.optional(),
525
544
  })
526
545
  .optional(),
527
546
  })
@@ -0,0 +1,177 @@
1
+ ---
2
+ import { Code } from 'astro-expressive-code/components';
3
+ import fs from 'fs';
4
+
5
+ import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
6
+ import Footer from '@layouts/Footer.astro';
7
+ import { Page } from './_[filename].data';
8
+ import { getAbsoluteFilePathForAstroFile } from '@utils/files';
9
+ import { buildUrl, buildEditUrlForResource } from '@utils/url-builder';
10
+ import Admonition from '@components/MDX/Admonition';
11
+
12
+ import { ServerIcon } from '@heroicons/react/24/outline';
13
+
14
+ export const prerender = Page.prerender;
15
+ export const getStaticPaths = Page.getStaticPaths;
16
+
17
+ // Get data
18
+ const { collection, data, filePath, filename } = await Page.getData(Astro);
19
+
20
+ const fileName = filename || 'schema.graphql';
21
+ const pathToSpec = getAbsoluteFilePathForAstroFile(filePath, fileName);
22
+ const fileExists = fs.existsSync(pathToSpec);
23
+ let content = '';
24
+
25
+ if (fileExists) {
26
+ content = fs.readFileSync(pathToSpec, 'utf8');
27
+ }
28
+
29
+ // Create comprehensive page title
30
+ const pageTitle = `${collection} | ${data.name} | GraphQL Schema`.replace(/^\w/, (c) => c.toUpperCase());
31
+
32
+ const getServiceBadge = () => {
33
+ return [{ backgroundColor: 'pink', textColor: 'pink', content: 'Service', icon: ServerIcon, class: 'text-pink-400' }];
34
+ };
35
+
36
+ const getGraphQLBadge = () => {
37
+ return [
38
+ {
39
+ backgroundColor: 'white',
40
+ textColor: 'gray',
41
+ content: 'GraphQL Schema',
42
+ iconURL: buildUrl('/icons/graphql.svg', true),
43
+ class: 'text-black',
44
+ id: 'graphql-schema-badge',
45
+ },
46
+ ];
47
+ };
48
+
49
+ const badges = [...getServiceBadge(), ...getGraphQLBadge()];
50
+
51
+ // Index only the latest version
52
+ const pagefindAttributes =
53
+ data.version === data.latestVersion
54
+ ? {
55
+ 'data-pagefind-body': '',
56
+ 'data-pagefind-meta': `title:${pageTitle}`,
57
+ }
58
+ : {};
59
+ ---
60
+
61
+ <VerticalSideBarLayout title={pageTitle} description={`GraphQL schema for ${data.name}`}>
62
+ <main class="flex sm:px-8 docs-layout h-full" {...pagefindAttributes}>
63
+ <div class="flex docs-layout w-full">
64
+ <div class="w-full lg:mr-2 pr-8 overflow-y-auto py-8">
65
+ <div class="border-b border-gray-200 md:pb-4">
66
+ <div>
67
+ <div class="flex justify-between items-start">
68
+ <div class="flex-1">
69
+ <h1 class="text-2xl md:text-4xl font-bold text-black mb-2">
70
+ {data.name}
71
+ <span class="text-gray-900">(v{data.version})</span>
72
+ </h1>
73
+ <h2 class="text-lg text-gray-600 font-medium mb-1">GraphQL Schema</h2>
74
+ </div>
75
+ </div>
76
+
77
+ {
78
+ badges && (
79
+ <div class="flex flex-wrap gap-3 py-4">
80
+ {badges.map((badge: any) => (
81
+ <span
82
+ id={badge.id || ''}
83
+ class={`
84
+ inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium
85
+ bg-${badge.backgroundColor || 'white'}-50 border border-${badge.backgroundColor || 'gray'}-200
86
+ text-${badge.textColor || 'gray'}-700 shadow-sm
87
+ transition-all duration-200 ease-out
88
+ ${badge.class ? badge.class : ''}
89
+ `}
90
+ >
91
+ {badge.icon && <badge.icon className={`w-4 h-4 flex-shrink-0 text-${badge.textColor || 'gray'}-600`} />}
92
+ {badge.iconURL && <img src={badge.iconURL} class="w-4 h-4 flex-shrink-0 opacity-80" alt="" />}
93
+ <span>{badge.content}</span>
94
+ </span>
95
+ ))}
96
+ </div>
97
+ )
98
+ }
99
+ </div>
100
+ </div>
101
+
102
+ <div data-pagefind-ignore>
103
+ {
104
+ data.version !== data.latestVersion && (
105
+ <div class="rounded-md bg-gradient-to-r from-purple-50 to-purple-100 p-4 not-prose my-4">
106
+ <div class="flex">
107
+ <div class="flex-shrink-0">
108
+ <svg class="h-5 w-5 text-purple-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
109
+ <path
110
+ fill-rule="evenodd"
111
+ d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z"
112
+ clip-rule="evenodd"
113
+ />
114
+ </svg>
115
+ </div>
116
+ <div class="ml-3">
117
+ <h3 class="text-sm font-medium text-purple-800">New version found</h3>
118
+ <div class="mt-2 text-sm text-purple-700">
119
+ <p>
120
+ You are looking at a previous version of the service <strong>{data.name}</strong>.{' '}
121
+ <a
122
+ class="underline hover:text-primary block pt-2"
123
+ href={buildUrl(`/docs/${collection}/${data.id}/${data.latestVersion}/graphql/${filename}`)}
124
+ >
125
+ The latest version of this GraphQL schema is
126
+ <span>v{data.latestVersion}</span> &rarr;
127
+ </a>
128
+ </p>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ )
134
+ }
135
+ </div>
136
+
137
+ {
138
+ !fileExists && (
139
+ <Admonition type="warning" title="Schema not found">
140
+ <p>The GraphQL schema file could not be found at the expected location.</p>
141
+ </Admonition>
142
+ )
143
+ }
144
+
145
+ {
146
+ fileExists && content && (
147
+ <div class="mt-6">
148
+ <div class="bg-gray-50 rounded-lg p-4 mb-4">
149
+ <div class="flex items-center gap-2 mb-2">
150
+ <img src={buildUrl('/icons/graphql.svg', true)} class="w-5 h-5" alt="GraphQL" />
151
+ <h3 class="text-lg font-semibold text-gray-800">GraphQL Schema</h3>
152
+ </div>
153
+ <p class="text-sm text-gray-600">
154
+ This schema defines the GraphQL API structure including types, queries, mutations, and subscriptions for{' '}
155
+ {data.name}.
156
+ </p>
157
+ </div>
158
+
159
+ <div class="not-prose overflow-x-auto">
160
+ <Code code={content} title={`${data.name} v${data.version} - Schema`} lang="graphql" />
161
+ </div>
162
+ </div>
163
+ )
164
+ }
165
+
166
+ <Footer />
167
+ </div>
168
+ </div>
169
+ </main>
170
+
171
+ <style is:global>
172
+ .docs-layout .prose {
173
+ max-width: none;
174
+ overflow: auto;
175
+ }
176
+ </style>
177
+ </VerticalSideBarLayout>
@@ -0,0 +1,98 @@
1
+ // pages/docs/[type]/[id]/[version]/asyncapi/[filename]/index.page.ts
2
+ import { isSSR, isAuthEnabled } from '@utils/feature';
3
+ import { HybridPage } from '@utils/page-loaders/hybrid-page';
4
+ import type { CollectionEntry } from 'astro:content';
5
+ import type { CollectionTypes, PageTypes } from '@types';
6
+
7
+ export class Page extends HybridPage {
8
+ static get prerender(): boolean {
9
+ return !isSSR();
10
+ }
11
+
12
+ static async getStaticPaths(): Promise<Array<{ params: any; props: any }>> {
13
+ if (isSSR()) {
14
+ return [];
15
+ }
16
+
17
+ const { pageDataLoader } = await import('@utils/page-loaders/page-data-loader');
18
+ const { getSpecificationsForService } = await import('@utils/collections/services');
19
+
20
+ const itemTypes: PageTypes[] = ['events', 'commands', 'queries', 'services', 'domains'];
21
+ const allItems = await Promise.all(itemTypes.map((type) => pageDataLoader[type]()));
22
+
23
+ const hasSpecifications = (item: CollectionEntry<CollectionTypes>) => {
24
+ const specifications = getSpecificationsForService(item);
25
+ // Ensure there is at least one 'asyncapi' specification
26
+ return specifications && specifications.some((spec) => spec.type === 'graphql');
27
+ };
28
+
29
+ const filteredItems = allItems.map((items) => items.filter(hasSpecifications));
30
+
31
+ return filteredItems.flatMap((items, index) =>
32
+ items.flatMap((item) => {
33
+ const asyncApiSpecifications = getSpecificationsForService(item).filter((spec) => spec.type === 'graphql');
34
+
35
+ return asyncApiSpecifications.map((spec) => ({
36
+ params: {
37
+ type: itemTypes[index],
38
+ id: item.data.id,
39
+ version: item.data.version,
40
+ filename: spec.filenameWithoutExtension || spec.type,
41
+ },
42
+ props: {
43
+ type: itemTypes[index],
44
+ filenameWithoutExtension: spec.filenameWithoutExtension || spec.type,
45
+ filename: spec.filename || spec.type,
46
+ ...item,
47
+ },
48
+ }));
49
+ })
50
+ );
51
+ }
52
+
53
+ protected static async fetchData(params: any) {
54
+ const { type, id, version, filename } = params;
55
+
56
+ if (!type || !id || !version || !filename) {
57
+ return null;
58
+ }
59
+
60
+ const { pageDataLoader } = await import('@utils/page-loaders/page-data-loader');
61
+ const { getSpecificationsForService } = await import('@utils/collections/services');
62
+
63
+ // Get all items of the specified type
64
+ const items = await pageDataLoader[type as PageTypes]();
65
+
66
+ // Find the specific item by id and version
67
+ const item = items.find((i) => i.data.id === id && i.data.version === version);
68
+
69
+ if (!item) {
70
+ return null;
71
+ }
72
+
73
+ // Check if this item has AsyncAPI specifications
74
+ const specifications = getSpecificationsForService(item);
75
+ const asyncApiSpecifications = specifications.filter((spec) => spec.type === 'graphql');
76
+
77
+ // Find the specific specification
78
+ const spec = asyncApiSpecifications.find((s) => (s.filenameWithoutExtension || s.type) === filename);
79
+
80
+ if (!spec) {
81
+ return null;
82
+ }
83
+
84
+ return {
85
+ type,
86
+ filenameWithoutExtension: spec.filenameWithoutExtension || spec.type,
87
+ filename: spec.filename || spec.type,
88
+ ...item,
89
+ };
90
+ }
91
+
92
+ protected static createNotFoundResponse(): Response {
93
+ return new Response(null, {
94
+ status: 404,
95
+ statusText: 'GraphQL specification not found',
96
+ });
97
+ }
98
+ }
@@ -125,6 +125,7 @@ const getSpecificationBadges = () => {
125
125
 
126
126
  const asyncapiSpecs = specifications.filter((spec) => spec.type === 'asyncapi');
127
127
  const openapiSpecs = specifications.filter((spec) => spec.type === 'openapi');
128
+ const graphQLSpecs = specifications.filter((spec) => spec.type === 'graphql');
128
129
 
129
130
  if (openapiSpecs.length > 0) {
130
131
  for (const spec of openapiSpecs) {
@@ -156,6 +157,22 @@ const getSpecificationBadges = () => {
156
157
  }
157
158
  }
158
159
 
160
+ if (graphQLSpecs.length > 0) {
161
+ for (const spec of graphQLSpecs) {
162
+ badges.push({
163
+ backgroundColor: 'white',
164
+ textColor: 'gray',
165
+ content: spec.name || 'GraphQL Spec',
166
+ iconURL: buildUrl('/icons/graphql.svg', true),
167
+ class: 'text-black hover:underline',
168
+ id: 'graphql-badge',
169
+ url: buildUrl(
170
+ `/docs/${props.collection}/${props.data.id}/${props.data.version}/graphql/${spec.filenameWithoutExtension}`
171
+ ),
172
+ });
173
+ }
174
+ }
175
+
159
176
  return badges;
160
177
  };
161
178
 
@@ -267,17 +284,37 @@ nodeGraphs.push({
267
284
  <h2 class="text-lg pt-2 text-gray-500 font-light">{props.data.summary}</h2>
268
285
  {
269
286
  badges && (
270
- <div class="flex flex-wrap py-2 pt-4">
287
+ <div class="flex flex-wrap gap-3 py-4">
271
288
  {badges.map((badge: any) => {
272
289
  return (
273
- <a href={badge.url || '#'} class="pb-2">
290
+ <a href={badge.url || '#'} class="group transition-all duration-200 hover:scale-105">
274
291
  <span
275
292
  id={badge.id || ''}
276
- class={`text-sm font-light text-gray-500 px-2 py-1 rounded-md mr-2 bg-gradient-to-b from-${badge.backgroundColor}-100 to-${badge.backgroundColor}-200 space-x-1 border border-${badge.backgroundColor}-200 text-${badge.textColor}-800 flex items-center ${badge.class ? badge.class : ''} `}
293
+ class={`
294
+ inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium
295
+ bg-${badge.backgroundColor || 'white'}-50 border border-${badge.backgroundColor || 'gray'}-200
296
+ text-${badge.textColor || 'gray'}-700 shadow-sm
297
+ hover:bg-${badge.backgroundColor || 'purple'}-100 hover:border-${badge.backgroundColor || 'purple'}-300
298
+ hover:shadow-md hover:text-${badge.textColor || 'purple'}-800
299
+ transition-all duration-200 ease-out
300
+ ${badge.class ? badge.class : ''}
301
+ `}
277
302
  >
278
- {badge.icon && <badge.icon className="w-4 h-4 inline-block mr-1 " />}
279
- {badge.iconURL && <img src={badge.iconURL} class="w-5 h-5 inline-block " />}
280
- <span>{badge.content}</span>
303
+ {badge.icon && (
304
+ <badge.icon
305
+ className={`w-4 h-4 flex-shrink-0 text-${badge.textColor || 'gray'}-600 group-hover:text-${badge.textColor || 'purple'}-700 transition-colors duration-200`}
306
+ />
307
+ )}
308
+ {badge.iconURL && (
309
+ <img
310
+ src={badge.iconURL}
311
+ class="w-4 h-4 flex-shrink-0 opacity-80 group-hover:opacity-100 transition-opacity duration-200"
312
+ alt=""
313
+ />
314
+ )}
315
+ <span class={`group-hover:text-${badge.textColor || 'purple'}-800 transition-colors duration-200`}>
316
+ {badge.content}
317
+ </span>
281
318
  </span>
282
319
  </a>
283
320
  );
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "url": "https://github.com/event-catalog/eventcatalog.git"
7
7
  },
8
8
  "type": "module",
9
- "version": "2.57.1",
9
+ "version": "2.58.0",
10
10
  "publishConfig": {
11
11
  "access": "public"
12
12
  },
@@ -129,6 +129,7 @@
129
129
  "prettier": "^3.3.3",
130
130
  "prettier-plugin-astro": "^0.14.1",
131
131
  "tsup": "^8.1.0",
132
+ "vite": "^7.1.7",
132
133
  "vite-tsconfig-paths": "^4.3.2",
133
134
  "vitest": "2.1.6"
134
135
  },