@eventcatalog/core 2.63.0 → 2.64.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 (47) 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-I2FMV7LN.js → chunk-6AMZOBWI.js} +1 -1
  6. package/dist/{chunk-IRFM5IS7.js → chunk-CWGFHLMX.js} +1 -1
  7. package/dist/{chunk-GA274FBN.js → chunk-PLMTJHGH.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 +2 -1
  13. package/eventcatalog/public/icons/avro.svg +21 -0
  14. package/eventcatalog/public/icons/json-schema.svg +6 -0
  15. package/eventcatalog/public/icons/proto.svg +10 -0
  16. package/eventcatalog/src/components/Grids/utils.tsx +5 -3
  17. package/eventcatalog/src/components/MDX/RemoteFile.astro +5 -11
  18. package/eventcatalog/src/components/MDX/SchemaViewer/SchemaViewerRoot.astro +41 -6
  19. package/eventcatalog/src/components/SchemaExplorer/ApiAccessSection.tsx +139 -0
  20. package/eventcatalog/src/components/SchemaExplorer/AvroSchemaViewer.tsx +423 -0
  21. package/eventcatalog/src/components/SchemaExplorer/DiffViewer.tsx +102 -0
  22. package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.tsx +740 -0
  23. package/eventcatalog/src/components/SchemaExplorer/OwnersSection.tsx +56 -0
  24. package/eventcatalog/src/components/SchemaExplorer/Pagination.tsx +33 -0
  25. package/eventcatalog/src/components/SchemaExplorer/ProducersConsumersSection.tsx +91 -0
  26. package/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx +93 -0
  27. package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +130 -0
  28. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsHeader.tsx +181 -0
  29. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +232 -0
  30. package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +415 -0
  31. package/eventcatalog/src/components/SchemaExplorer/SchemaFilters.tsx +174 -0
  32. package/eventcatalog/src/components/SchemaExplorer/SchemaListItem.tsx +73 -0
  33. package/eventcatalog/src/components/SchemaExplorer/SchemaViewerModal.tsx +77 -0
  34. package/eventcatalog/src/components/SchemaExplorer/VersionHistoryModal.tsx +72 -0
  35. package/eventcatalog/src/components/SchemaExplorer/types.ts +45 -0
  36. package/eventcatalog/src/components/SchemaExplorer/utils.ts +81 -0
  37. package/eventcatalog/src/components/SideNav/ListViewSideBar/index.tsx +33 -2
  38. package/eventcatalog/src/components/Tables/columns/MessageTableColumns.tsx +2 -2
  39. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +10 -0
  40. package/eventcatalog/src/pages/api/schemas/[collection]/[id]/[version]/index.ts +45 -0
  41. package/eventcatalog/src/pages/api/schemas/services/[id]/[version]/[specification]/index.ts +51 -0
  42. package/eventcatalog/src/pages/docs/llm/schemas.txt.ts +86 -0
  43. package/eventcatalog/src/pages/schemas/index.astro +175 -0
  44. package/eventcatalog/src/utils/files.ts +9 -0
  45. package/package.json +1 -1
  46. package/eventcatalog/src/components/MDX/SchemaViewer/SchemaProperty.astro +0 -204
  47. package/eventcatalog/src/components/MDX/SchemaViewer/SchemaViewer.astro +0 -705
@@ -0,0 +1,174 @@
1
+ import { MagnifyingGlassIcon, XMarkIcon, FunnelIcon, ChevronUpIcon, ChevronDownIcon } from '@heroicons/react/24/outline';
2
+ import type { CollectionMessageTypes } from '@types';
3
+ import { getSchemaTypeLabel } from './utils';
4
+ import type { SchemaItem } from './types';
5
+
6
+ interface SchemaFiltersProps {
7
+ searchQuery: string;
8
+ onSearchChange: (query: string) => void;
9
+ selectedType: 'all' | CollectionMessageTypes | 'services';
10
+ onTypeChange: (type: 'all' | CollectionMessageTypes | 'services') => void;
11
+ selectedSchemaType: string;
12
+ onSchemaTypeChange: (type: string) => void;
13
+ schemaTypes: string[];
14
+ latestMessages: SchemaItem[];
15
+ filtersExpanded: boolean;
16
+ onToggleExpanded: () => void;
17
+ searchInputRef: React.RefObject<HTMLInputElement>;
18
+ isMounted: boolean;
19
+ }
20
+
21
+ export default function SchemaFilters({
22
+ searchQuery,
23
+ onSearchChange,
24
+ selectedType,
25
+ onTypeChange,
26
+ selectedSchemaType,
27
+ onSchemaTypeChange,
28
+ schemaTypes,
29
+ latestMessages,
30
+ filtersExpanded,
31
+ onToggleExpanded,
32
+ searchInputRef,
33
+ isMounted,
34
+ }: SchemaFiltersProps) {
35
+ const activeFilterCount = [searchQuery, selectedType !== 'all', selectedSchemaType !== 'all'].filter(Boolean).length;
36
+
37
+ return (
38
+ <div className="flex-shrink-0 border-b border-gray-200 bg-gray-50">
39
+ {/* Filter Header */}
40
+ <button
41
+ onClick={onToggleExpanded}
42
+ className="w-full flex items-center justify-between p-3 hover:bg-gray-100 transition-colors"
43
+ >
44
+ <div className="flex items-center gap-2">
45
+ <FunnelIcon className="h-4 w-4 text-gray-600" />
46
+ <span className="text-xs font-semibold text-gray-900">Filters</span>
47
+ {activeFilterCount > 0 && (
48
+ <span className="inline-flex items-center rounded-full bg-primary px-2 py-0.5 text-xs font-medium text-white">
49
+ {activeFilterCount}
50
+ </span>
51
+ )}
52
+ </div>
53
+ {filtersExpanded ? (
54
+ <ChevronUpIcon className="h-4 w-4 text-gray-600" />
55
+ ) : (
56
+ <ChevronDownIcon className="h-4 w-4 text-gray-600" />
57
+ )}
58
+ </button>
59
+
60
+ {/* Collapsible Filter Content - Only render after mount to prevent FOUC */}
61
+ {isMounted && filtersExpanded && (
62
+ <div className="p-3 pt-0">
63
+ {/* Search */}
64
+ <div className="mb-3">
65
+ <label htmlFor="search" className="block text-xs font-medium text-gray-700 mb-1.5">
66
+ Search
67
+ </label>
68
+ <div className="relative">
69
+ <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-2.5">
70
+ <MagnifyingGlassIcon className="h-4 w-4 text-gray-400" />
71
+ </div>
72
+ <input
73
+ ref={searchInputRef}
74
+ type="text"
75
+ id="search"
76
+ placeholder="Search schemas..."
77
+ value={searchQuery}
78
+ onChange={(e) => onSearchChange(e.target.value)}
79
+ className="w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring-primary text-xs pl-8 pr-8 py-1.5 border"
80
+ />
81
+ {searchQuery && (
82
+ <button onClick={() => onSearchChange('')} className="absolute inset-y-0 right-0 flex items-center pr-2.5">
83
+ <XMarkIcon className="h-4 w-4 text-gray-400 hover:text-gray-600" />
84
+ </button>
85
+ )}
86
+ </div>
87
+ </div>
88
+
89
+ {/* Message Type Filter */}
90
+ <div className="mb-3">
91
+ <label htmlFor="messageType" className="block text-xs font-medium text-gray-700 mb-1.5">
92
+ Message Type
93
+ </label>
94
+ <select
95
+ id="messageType"
96
+ value={selectedType}
97
+ onChange={(e) => onTypeChange(e.target.value as typeof selectedType)}
98
+ className="w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring-primary text-xs px-2.5 py-1.5 border"
99
+ >
100
+ <option value="all">All ({latestMessages.length})</option>
101
+ <option value="events">Events ({latestMessages.filter((m) => m.collection === 'events').length})</option>
102
+ <option value="commands">Commands ({latestMessages.filter((m) => m.collection === 'commands').length})</option>
103
+ <option value="queries">Queries ({latestMessages.filter((m) => m.collection === 'queries').length})</option>
104
+ <option value="services">Services ({latestMessages.filter((m) => m.collection === 'services').length})</option>
105
+ </select>
106
+ </div>
107
+
108
+ {/* Schema Type Filter */}
109
+ <div className="mb-3">
110
+ <label htmlFor="schemaType" className="block text-xs font-medium text-gray-700 mb-1.5">
111
+ Schema Format
112
+ </label>
113
+ <select
114
+ id="schemaType"
115
+ value={selectedSchemaType}
116
+ onChange={(e) => onSchemaTypeChange(e.target.value)}
117
+ className="w-full rounded-md border-gray-300 shadow-sm focus:border-primary focus:ring-primary text-xs px-2.5 py-1.5 border"
118
+ >
119
+ <option value="all">All Formats</option>
120
+ {schemaTypes.map((type) => (
121
+ <option key={type} value={type}>
122
+ {getSchemaTypeLabel(type)} ({latestMessages.filter((m) => m.schemaExtension?.toLowerCase() === type).length})
123
+ </option>
124
+ ))}
125
+ </select>
126
+ </div>
127
+
128
+ {/* Active filters */}
129
+ {activeFilterCount > 0 && (
130
+ <div className="pt-2 border-t border-gray-200">
131
+ <div className="flex flex-wrap items-center gap-1.5">
132
+ {searchQuery && (
133
+ <span className="inline-flex items-center gap-1 px-1.5 py-0.5 text-xs bg-blue-100 text-blue-800 rounded">
134
+ {searchQuery.substring(0, 15)}
135
+ {searchQuery.length > 15 ? '...' : ''}
136
+ <button onClick={() => onSearchChange('')}>
137
+ <XMarkIcon className="h-3 w-3" />
138
+ </button>
139
+ </span>
140
+ )}
141
+ {selectedType !== 'all' && (
142
+ <span className="inline-flex items-center gap-1 px-1.5 py-0.5 text-xs bg-blue-100 text-blue-800 rounded">
143
+ {selectedType}
144
+ <button onClick={() => onTypeChange('all')}>
145
+ <XMarkIcon className="h-3 w-3" />
146
+ </button>
147
+ </span>
148
+ )}
149
+ {selectedSchemaType !== 'all' && (
150
+ <span className="inline-flex items-center gap-1 px-1.5 py-0.5 text-xs bg-blue-100 text-blue-800 rounded">
151
+ {getSchemaTypeLabel(selectedSchemaType)}
152
+ <button onClick={() => onSchemaTypeChange('all')}>
153
+ <XMarkIcon className="h-3 w-3" />
154
+ </button>
155
+ </span>
156
+ )}
157
+ <button
158
+ onClick={() => {
159
+ onSearchChange('');
160
+ onTypeChange('all');
161
+ onSchemaTypeChange('all');
162
+ }}
163
+ className="text-xs text-gray-600 hover:text-gray-900 underline"
164
+ >
165
+ Clear
166
+ </button>
167
+ </div>
168
+ </div>
169
+ )}
170
+ </div>
171
+ )}
172
+ </div>
173
+ );
174
+ }
@@ -0,0 +1,73 @@
1
+ import { buildUrl } from '@utils/url-builder';
2
+ import { getCollectionStyles } from '@components/Grids/utils';
3
+ import { getSchemaTypeLabel } from './utils';
4
+ import type { SchemaItem } from './types';
5
+
6
+ interface SchemaListItemProps {
7
+ message: SchemaItem;
8
+ isSelected: boolean;
9
+ versions: SchemaItem[];
10
+ onClick: () => void;
11
+ itemRef?: React.RefObject<HTMLButtonElement>;
12
+ }
13
+
14
+ export default function SchemaListItem({ message, isSelected, versions, onClick, itemRef }: SchemaListItemProps) {
15
+ const { color, Icon } = getCollectionStyles(message.collection);
16
+ const hasMultipleVersions = versions.length > 1;
17
+
18
+ return (
19
+ <button
20
+ ref={itemRef}
21
+ onClick={onClick}
22
+ className={`w-full text-left p-4 hover:bg-gray-50 transition-colors ${
23
+ isSelected ? `bg-${color}-50 border-l-4 border-${color}-500` : 'border-l-4 border-transparent'
24
+ }`}
25
+ >
26
+ <div className="flex items-start gap-3">
27
+ <Icon className={`h-5 w-5 mt-0.5 flex-shrink-0 ${isSelected ? `text-${color}-600` : `text-${color}-500`}`} />
28
+ <div className="flex-1 min-w-0">
29
+ <div className="flex items-center justify-between gap-2 mb-1.5">
30
+ <div className="flex items-center gap-2 min-w-0">
31
+ <h3 className={`text-sm font-semibold truncate ${isSelected ? `text-${color}-900` : 'text-gray-900'}`}>
32
+ {message.data.name}
33
+ </h3>
34
+ <span className="text-xs text-gray-500 flex-shrink-0">v{message.data.version}</span>
35
+ </div>
36
+ <div className="flex items-center gap-1 flex-shrink-0">
37
+ <span className="inline-flex items-center gap-1 rounded-full bg-gray-100 px-1.5 py-0.5 text-xs font-medium text-gray-800">
38
+ {(() => {
39
+ const ext = message.schemaExtension?.toLowerCase();
40
+ if (
41
+ ext === 'openapi' ||
42
+ ext === 'asyncapi' ||
43
+ ext === 'graphql' ||
44
+ ext === 'avro' ||
45
+ ext === 'json' ||
46
+ ext === 'proto'
47
+ ) {
48
+ // Map json extension to json-schema icon
49
+ const iconName = ext === 'json' ? 'json-schema' : ext;
50
+ const iconPath = buildUrl(`/icons/${iconName}.svg`, true);
51
+ return (
52
+ <>
53
+ <img src={iconPath} alt={`${ext} icon`} className="h-3 w-3" />
54
+ {getSchemaTypeLabel(message.schemaExtension)}
55
+ </>
56
+ );
57
+ }
58
+ return getSchemaTypeLabel(message.schemaExtension);
59
+ })()}
60
+ </span>
61
+ {hasMultipleVersions && (
62
+ <span className="inline-flex items-center rounded-full bg-blue-100 px-1 py-0.5 text-xs font-medium text-blue-700">
63
+ {versions.length} versions
64
+ </span>
65
+ )}
66
+ </div>
67
+ </div>
68
+ {message.data.summary && <p className="text-xs text-gray-600 line-clamp-2">{message.data.summary}</p>}
69
+ </div>
70
+ </div>
71
+ </button>
72
+ );
73
+ }
@@ -0,0 +1,77 @@
1
+ import * as Dialog from '@radix-ui/react-dialog';
2
+ import { XMarkIcon, ArrowsPointingOutIcon } from '@heroicons/react/24/outline';
3
+ import JSONSchemaViewer from './JSONSchemaViewer';
4
+ import AvroSchemaViewer from './AvroSchemaViewer';
5
+ import type { SchemaItem } from './types';
6
+
7
+ interface SchemaViewerModalProps {
8
+ isOpen: boolean;
9
+ onOpenChange: (open: boolean) => void;
10
+ message: SchemaItem;
11
+ parsedSchema: any;
12
+ parsedAvroSchema?: any;
13
+ }
14
+
15
+ export default function SchemaViewerModal({
16
+ isOpen,
17
+ onOpenChange,
18
+ message,
19
+ parsedSchema,
20
+ parsedAvroSchema,
21
+ }: SchemaViewerModalProps) {
22
+ if (!parsedSchema && !parsedAvroSchema) return null;
23
+
24
+ const isAvro = !!parsedAvroSchema;
25
+
26
+ return (
27
+ <Dialog.Root open={isOpen} onOpenChange={onOpenChange}>
28
+ <Dialog.Portal>
29
+ <Dialog.Overlay className="fixed inset-0 bg-black/50 data-[state=open]:animate-overlayShow z-50" />
30
+ <Dialog.Content className="fixed inset-4 md:inset-8 rounded-lg bg-white shadow-xl focus:outline-none data-[state=open]:animate-contentShow z-[100] flex flex-col">
31
+ {/* Header */}
32
+ <div className="flex items-center justify-between p-6 border-b border-gray-200 flex-shrink-0">
33
+ <div className="flex items-center gap-3">
34
+ <ArrowsPointingOutIcon className="h-6 w-6 text-gray-500" />
35
+ <div>
36
+ <Dialog.Title className="text-xl font-semibold text-gray-900">{message.data.name}</Dialog.Title>
37
+ <Dialog.Description className="text-sm text-gray-600 mt-1">
38
+ v{message.data.version} · {isAvro ? 'Avro' : 'JSON'} Schema
39
+ </Dialog.Description>
40
+ </div>
41
+ </div>
42
+ <Dialog.Close asChild>
43
+ <button
44
+ type="button"
45
+ className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
46
+ aria-label="Close"
47
+ >
48
+ <XMarkIcon className="h-6 w-6" />
49
+ </button>
50
+ </Dialog.Close>
51
+ </div>
52
+
53
+ {/* Content */}
54
+ <div className="flex-1 overflow-hidden p-6">
55
+ {isAvro ? (
56
+ <AvroSchemaViewer schema={parsedAvroSchema} expand={true} search={true} />
57
+ ) : (
58
+ <JSONSchemaViewer schema={parsedSchema} expand={true} search={true} />
59
+ )}
60
+ </div>
61
+
62
+ {/* Footer */}
63
+ <div className="flex justify-end p-4 border-t border-gray-200 flex-shrink-0">
64
+ <Dialog.Close asChild>
65
+ <button
66
+ type="button"
67
+ className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors"
68
+ >
69
+ Close
70
+ </button>
71
+ </Dialog.Close>
72
+ </div>
73
+ </Dialog.Content>
74
+ </Dialog.Portal>
75
+ </Dialog.Root>
76
+ );
77
+ }
@@ -0,0 +1,72 @@
1
+ import * as Dialog from '@radix-ui/react-dialog';
2
+ import { XMarkIcon, ArrowsPointingOutIcon } from '@heroicons/react/24/outline';
3
+ import DiffViewer from './DiffViewer';
4
+ import type { VersionDiff } from './types';
5
+
6
+ interface VersionHistoryModalProps {
7
+ isOpen: boolean;
8
+ onOpenChange: (open: boolean) => void;
9
+ diffs: VersionDiff[];
10
+ messageName: string;
11
+ apiAccessEnabled?: boolean;
12
+ }
13
+
14
+ export default function VersionHistoryModal({
15
+ isOpen,
16
+ onOpenChange,
17
+ diffs,
18
+ messageName,
19
+ apiAccessEnabled = false,
20
+ }: VersionHistoryModalProps) {
21
+ return (
22
+ <Dialog.Root open={isOpen} onOpenChange={onOpenChange}>
23
+ <Dialog.Portal>
24
+ <Dialog.Overlay className="fixed inset-0 bg-black/50 data-[state=open]:animate-overlayShow z-50" />
25
+ <Dialog.Content className="fixed inset-4 md:inset-8 rounded-lg bg-white shadow-xl focus:outline-none data-[state=open]:animate-contentShow z-[100] flex flex-col">
26
+ {/* Header */}
27
+ <div className="flex items-center justify-between p-6 border-b border-gray-200 flex-shrink-0">
28
+ <div className="flex items-center gap-3">
29
+ <ArrowsPointingOutIcon className="h-6 w-6 text-gray-500" />
30
+ <div>
31
+ <Dialog.Title className="text-xl font-semibold text-gray-900">Version History</Dialog.Title>
32
+ <Dialog.Description className="text-sm text-gray-600 mt-1">{messageName}</Dialog.Description>
33
+ </div>
34
+ </div>
35
+ <Dialog.Close asChild>
36
+ <button
37
+ type="button"
38
+ className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-md transition-colors"
39
+ aria-label="Close"
40
+ >
41
+ <XMarkIcon className="h-6 w-6" />
42
+ </button>
43
+ </Dialog.Close>
44
+ </div>
45
+
46
+ {/* Content */}
47
+ <div className="flex-1 overflow-y-auto p-6">
48
+ {diffs.length > 0 ? (
49
+ <DiffViewer diffs={diffs} apiAccessEnabled={apiAccessEnabled} />
50
+ ) : (
51
+ <div className="flex items-center justify-center h-full">
52
+ <p className="text-gray-500 text-center">No version history available</p>
53
+ </div>
54
+ )}
55
+ </div>
56
+
57
+ {/* Footer */}
58
+ <div className="flex justify-end p-4 border-t border-gray-200 flex-shrink-0">
59
+ <Dialog.Close asChild>
60
+ <button
61
+ type="button"
62
+ className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 transition-colors"
63
+ >
64
+ Close
65
+ </button>
66
+ </Dialog.Close>
67
+ </div>
68
+ </Dialog.Content>
69
+ </Dialog.Portal>
70
+ </Dialog.Root>
71
+ );
72
+ }
@@ -0,0 +1,45 @@
1
+ import type { CollectionMessageTypes } from '@types';
2
+
3
+ export interface Producer {
4
+ id: string;
5
+ version: string;
6
+ }
7
+
8
+ export interface Consumer {
9
+ id: string;
10
+ version: string;
11
+ }
12
+
13
+ export interface Owner {
14
+ id: string;
15
+ name: string;
16
+ type: 'users' | 'teams';
17
+ href: string;
18
+ }
19
+
20
+ export interface SchemaItem {
21
+ collection: CollectionMessageTypes | 'services';
22
+ data: {
23
+ id: string;
24
+ name: string;
25
+ version: string;
26
+ summary?: string;
27
+ schemaPath?: string;
28
+ producers?: Producer[];
29
+ consumers?: Consumer[];
30
+ owners?: Owner[];
31
+ };
32
+ schemaContent?: string;
33
+ schemaExtension?: string;
34
+ specType?: string;
35
+ specName?: string;
36
+ specFilenameWithoutExtension?: string;
37
+ }
38
+
39
+ export interface VersionDiff {
40
+ newerVersion: string;
41
+ olderVersion: string;
42
+ diffHtml: string;
43
+ newerContent: string;
44
+ olderContent: string;
45
+ }
@@ -0,0 +1,81 @@
1
+ export const getLanguageForHighlight = (extension?: string): string => {
2
+ if (!extension) return 'json';
3
+ const ext = extension.toLowerCase();
4
+ switch (ext) {
5
+ case 'avro':
6
+ case 'avsc':
7
+ case 'json':
8
+ return 'json';
9
+ case 'proto':
10
+ return 'protobuf';
11
+ case 'xsd':
12
+ case 'xml':
13
+ return 'xml';
14
+ case 'graphql':
15
+ case 'gql':
16
+ return 'graphql';
17
+ case 'yaml':
18
+ case 'yml':
19
+ case 'openapi':
20
+ case 'asyncapi':
21
+ return 'yaml';
22
+ case 'ts':
23
+ case 'typescript':
24
+ return 'typescript';
25
+ case 'js':
26
+ case 'javascript':
27
+ return 'javascript';
28
+ default:
29
+ return 'json';
30
+ }
31
+ };
32
+
33
+ export const getSchemaTypeLabel = (extension?: string): string => {
34
+ if (!extension) return 'JSON';
35
+ const ext = extension.toLowerCase();
36
+ switch (ext) {
37
+ case 'avro':
38
+ case 'avsc':
39
+ return 'Avro';
40
+ case 'proto':
41
+ return 'Protobuf';
42
+ case 'xsd':
43
+ return 'XML Schema';
44
+ case 'graphql':
45
+ case 'gql':
46
+ return 'GraphQL';
47
+ case 'yaml':
48
+ case 'yml':
49
+ return 'YAML';
50
+ case 'json':
51
+ return 'JSON Schema';
52
+ case 'openapi':
53
+ return 'OpenAPI';
54
+ case 'asyncapi':
55
+ return 'AsyncAPI';
56
+ default:
57
+ return ext.toUpperCase();
58
+ }
59
+ };
60
+
61
+ export const copyToClipboard = async (content: string): Promise<boolean> => {
62
+ try {
63
+ await navigator.clipboard.writeText(content);
64
+ return true;
65
+ } catch (err) {
66
+ console.error('Failed to copy:', err);
67
+ return false;
68
+ }
69
+ };
70
+
71
+ export const downloadSchema = (content: string, filename: string, extension: string) => {
72
+ const blob = new Blob([content], { type: 'text/plain' });
73
+ const url = URL.createObjectURL(blob);
74
+ const a = document.createElement('a');
75
+ a.href = url;
76
+ a.download = `${filename}.${extension}`;
77
+ document.body.appendChild(a);
78
+ a.click();
79
+ document.body.removeChild(a);
80
+ URL.revokeObjectURL(url);
81
+ };
@@ -312,6 +312,8 @@ const ListViewSideBar: React.FC<ListViewSideBarProps> = ({ resources, currentPat
312
312
  const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
313
313
  const [isInitialized, setIsInitialized] = useState(false);
314
314
  const [isExpanded, setIsExpanded] = useState(true);
315
+ const [isSearchPinned, setIsSearchPinned] = useState(false);
316
+ const [lastScrollTop, setLastScrollTop] = useState(0);
315
317
  const [collapsedGroups, setCollapsedGroups] = useState<{ [key: string]: boolean }>(() => {
316
318
  if (typeof window !== 'undefined') {
317
319
  const saved = window.localStorage.getItem(STORAGE_KEY);
@@ -471,6 +473,31 @@ const ListViewSideBar: React.FC<ListViewSideBarProps> = ({ resources, currentPat
471
473
  }
472
474
  }, [debouncedSearchTerm]);
473
475
 
476
+ // Handle scroll for sticky search bar
477
+ useEffect(() => {
478
+ const nav = navRef.current;
479
+ if (!nav) return;
480
+
481
+ const handleScroll = () => {
482
+ const scrollTop = nav.scrollTop;
483
+ const scrollThreshold = 50; // Pin after scrolling 50px
484
+
485
+ // Scrolling down past threshold
486
+ if (scrollTop > scrollThreshold && scrollTop > lastScrollTop) {
487
+ setIsSearchPinned(true);
488
+ }
489
+ // Scrolling up near the top
490
+ else if (scrollTop <= scrollThreshold) {
491
+ setIsSearchPinned(false);
492
+ }
493
+
494
+ setLastScrollTop(scrollTop);
495
+ };
496
+
497
+ nav.addEventListener('scroll', handleScroll);
498
+ return () => nav.removeEventListener('scroll', handleScroll);
499
+ }, [lastScrollTop]);
500
+
474
501
  // Store collapsed groups in local storage
475
502
  useEffect(() => {
476
503
  if (typeof window !== 'undefined') {
@@ -872,8 +899,12 @@ const ListViewSideBar: React.FC<ListViewSideBarProps> = ({ resources, currentPat
872
899
  !filteredData.messagesNotInService?.length;
873
900
 
874
901
  return (
875
- <nav ref={navRef} className="space-y-4 text-gray-800 px-3 py-4">
876
- <div className="flex gap-2">
902
+ <nav ref={navRef} className="space-y-4 text-gray-800 px-3 py-4 overflow-auto h-full">
903
+ <div
904
+ className={`flex gap-2 transition-all duration-200 ${
905
+ isSearchPinned ? 'sticky -top-5 z-10 bg-white shadow-md -mx-3 px-3 py-2 border-b border-gray-200' : ''
906
+ }`}
907
+ >
877
908
  <input
878
909
  type="text"
879
910
  value={searchTerm}
@@ -74,7 +74,7 @@ export const columns = (tableConfiguration: TableConfiguration) => [
74
74
 
75
75
  columnHelper.accessor('data.producers', {
76
76
  id: 'producers',
77
- header: () => <span>Producers</span>,
77
+ header: () => <span>{tableConfiguration.columns?.producers?.label || 'Producers'}</span>,
78
78
  meta: {
79
79
  filterVariant: 'collection',
80
80
  collectionFilterKey: 'producers',
@@ -114,7 +114,7 @@ export const columns = (tableConfiguration: TableConfiguration) => [
114
114
  }),
115
115
  columnHelper.accessor('data.consumers', {
116
116
  id: 'consumers',
117
- header: () => <span>Consumers</span>,
117
+ header: () => <span>{tableConfiguration.columns?.consumers?.label || 'Consumers'}</span>,
118
118
  meta: {
119
119
  filterVariant: 'collection',
120
120
  collectionFilterKey: 'consumers',
@@ -17,6 +17,8 @@ import {
17
17
  Rocket,
18
18
  FileText,
19
19
  SquareDashedMousePointerIcon,
20
+ PackageSearch,
21
+ FileJson,
20
22
  } from 'lucide-react';
21
23
  import Header from '../components/Header.astro';
22
24
  import SEO from '../components/Seo.astro';
@@ -121,6 +123,14 @@ const navigationItems = [
121
123
  current: currentPath.includes('/discover/'),
122
124
  sidebar: false,
123
125
  },
126
+ {
127
+ id: '/schemas',
128
+ label: 'Schema Explorer',
129
+ icon: FileJson,
130
+ href: buildUrl('/schemas'),
131
+ current: currentPath.includes('/schemas'),
132
+ sidebar: false,
133
+ },
124
134
  {
125
135
  id: '/directory',
126
136
  label: 'Users & Teams',
@@ -0,0 +1,45 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { getCollection } from 'astro:content';
3
+ import path from 'node:path';
4
+ import fs from 'node:fs';
5
+ import { isEventCatalogScaleEnabled } from '@utils/feature';
6
+
7
+ export async function getStaticPaths() {
8
+ const events = await getCollection('events');
9
+ const commands = await getCollection('commands');
10
+ const queries = await getCollection('queries');
11
+ const messages = [...events, ...commands, ...queries];
12
+ return messages
13
+ .filter((message) => message.data.schemaPath)
14
+ .filter((message) => fs.existsSync(path.join(path.dirname(message.filePath ?? ''), message.data.schemaPath ?? '')))
15
+ .map((message) => ({
16
+ params: { collection: message.collection, id: message.data.id, version: message.data.version },
17
+ props: {
18
+ pathToSchema: path.join(path.dirname(message.filePath ?? ''), message.data.schemaPath ?? ''),
19
+ schema: fs.readFileSync(path.join(path.dirname(message.filePath ?? ''), message.data.schemaPath ?? ''), 'utf8'),
20
+ extension: message.data.schemaPath?.split('.').pop(),
21
+ },
22
+ }));
23
+ }
24
+
25
+ export const GET: APIRoute = async ({ props }) => {
26
+ if (!isEventCatalogScaleEnabled()) {
27
+ return new Response(
28
+ JSON.stringify({
29
+ error: 'feature_not_available_on_server',
30
+ message: 'Schema API is not enabled for this deployment and supported in EventCatalog Scale.',
31
+ }),
32
+ {
33
+ status: 501,
34
+ headers: {
35
+ 'Content-Type': 'application/json',
36
+ 'Cache-Control': 'no-store',
37
+ },
38
+ }
39
+ );
40
+ }
41
+
42
+ return new Response(props.schema, {
43
+ headers: { 'Content-Type': 'text/plain' },
44
+ });
45
+ };