@eventcatalog/core 2.63.0 → 2.64.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/{chunk-GA274FBN.js → chunk-AHJ4UE33.js} +1 -1
- package/dist/{chunk-IRFM5IS7.js → chunk-LCBQ5JUR.js} +1 -1
- package/dist/{chunk-I2FMV7LN.js → chunk-SH6FZS4K.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +3 -3
- package/eventcatalog/astro.config.mjs +2 -1
- package/eventcatalog/public/icons/avro.svg +21 -0
- package/eventcatalog/public/icons/json-schema.svg +6 -0
- package/eventcatalog/public/icons/proto.svg +10 -0
- package/eventcatalog/src/components/Grids/utils.tsx +5 -3
- package/eventcatalog/src/components/MDX/RemoteFile.astro +5 -11
- package/eventcatalog/src/components/MDX/SchemaViewer/SchemaViewerRoot.astro +41 -6
- package/eventcatalog/src/components/SchemaExplorer/ApiAccessSection.tsx +139 -0
- package/eventcatalog/src/components/SchemaExplorer/AvroSchemaViewer.tsx +465 -0
- package/eventcatalog/src/components/SchemaExplorer/DiffViewer.tsx +102 -0
- package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.tsx +740 -0
- package/eventcatalog/src/components/SchemaExplorer/OwnersSection.tsx +56 -0
- package/eventcatalog/src/components/SchemaExplorer/Pagination.tsx +33 -0
- package/eventcatalog/src/components/SchemaExplorer/ProducersConsumersSection.tsx +91 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx +93 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +132 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsHeader.tsx +181 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +233 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +415 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaFilters.tsx +174 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaListItem.tsx +73 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaViewerModal.tsx +77 -0
- package/eventcatalog/src/components/SchemaExplorer/VersionHistoryModal.tsx +72 -0
- package/eventcatalog/src/components/SchemaExplorer/types.ts +45 -0
- package/eventcatalog/src/components/SchemaExplorer/utils.ts +81 -0
- package/eventcatalog/src/components/SideNav/ListViewSideBar/index.tsx +33 -2
- package/eventcatalog/src/components/Tables/columns/MessageTableColumns.tsx +2 -2
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +10 -0
- package/eventcatalog/src/pages/api/schemas/[collection]/[id]/[version]/index.ts +45 -0
- package/eventcatalog/src/pages/api/schemas/services/[id]/[version]/[specification]/index.ts +51 -0
- package/eventcatalog/src/pages/docs/llm/schemas.txt.ts +86 -0
- package/eventcatalog/src/pages/schemas/index.astro +175 -0
- package/eventcatalog/src/utils/files.ts +9 -0
- package/package.json +1 -1
- package/eventcatalog/src/components/MDX/SchemaViewer/SchemaProperty.astro +0 -204
- 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
|
|
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
|
+
};
|