@eventcatalog/core 2.62.1 → 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.
- 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-4BYDUGYI.js → chunk-6AMZOBWI.js} +1 -1
- package/dist/{chunk-GZ2SVHEA.js → chunk-CWGFHLMX.js} +1 -1
- package/dist/{chunk-IWFL6VRS.js → chunk-PLMTJHGH.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.config.d.cts +34 -0
- package/dist/eventcatalog.config.d.ts +34 -0
- 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 +423 -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 +130 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsHeader.tsx +181 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +232 -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/Table.tsx +10 -2
- package/eventcatalog/src/components/Tables/columns/ContainersTableColumns.tsx +10 -8
- package/eventcatalog/src/components/Tables/columns/DomainTableColumns.tsx +8 -6
- package/eventcatalog/src/components/Tables/columns/FlowTableColumns.tsx +9 -7
- package/eventcatalog/src/components/Tables/columns/MessageTableColumns.tsx +11 -9
- package/eventcatalog/src/components/Tables/columns/ServiceTableColumns.tsx +9 -7
- package/eventcatalog/src/components/Tables/columns/SharedColumns.tsx +4 -2
- package/eventcatalog/src/components/Tables/columns/TeamsTableColumns.tsx +12 -8
- package/eventcatalog/src/components/Tables/columns/UserTableColumns.tsx +14 -9
- package/eventcatalog/src/components/Tables/columns/index.tsx +9 -8
- package/eventcatalog/src/content.config.ts +3 -2
- package/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro +1 -0
- package/eventcatalog/src/layouts/DirectoryLayout.astro +21 -22
- package/eventcatalog/src/layouts/DiscoverLayout.astro +26 -12
- 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/[type]/[id]/[version]/index.astro +1 -0
- package/eventcatalog/src/pages/docs/llm/schemas.txt.ts +86 -0
- package/eventcatalog/src/pages/schemas/index.astro +175 -0
- package/eventcatalog/src/types/index.ts +9 -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}
|
|
@@ -14,7 +14,7 @@ import DebouncedInput from './DebouncedInput';
|
|
|
14
14
|
|
|
15
15
|
import { getColumnsByCollection } from './columns';
|
|
16
16
|
import { useEffect, useMemo, useState } from 'react';
|
|
17
|
-
import type { CollectionMessageTypes } from '@types';
|
|
17
|
+
import type { CollectionMessageTypes, TableConfiguration } from '@types';
|
|
18
18
|
import { isSameVersion } from '@utils/collections/util';
|
|
19
19
|
|
|
20
20
|
declare module '@tanstack/react-table' {
|
|
@@ -146,12 +146,14 @@ export const Table = <T extends TCollectionTypes>({
|
|
|
146
146
|
mode = 'simple',
|
|
147
147
|
checkboxLatestId,
|
|
148
148
|
checkboxDraftsId,
|
|
149
|
+
tableConfiguration,
|
|
149
150
|
}: {
|
|
150
151
|
data: TData<T>[];
|
|
151
152
|
collection: T;
|
|
152
153
|
checkboxLatestId: string;
|
|
153
154
|
checkboxDraftsId: string;
|
|
154
155
|
mode?: 'simple' | 'full';
|
|
156
|
+
tableConfiguration?: TableConfiguration;
|
|
155
157
|
}) => {
|
|
156
158
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
|
157
159
|
|
|
@@ -215,7 +217,10 @@ export const Table = <T extends TCollectionTypes>({
|
|
|
215
217
|
});
|
|
216
218
|
}, [initialData, showOnlyLatest, onlyShowDrafts]);
|
|
217
219
|
|
|
218
|
-
const columns = useMemo(
|
|
220
|
+
const columns = useMemo(
|
|
221
|
+
() => getColumnsByCollection(collection, tableConfiguration ?? ({ columns: {} } as TableConfiguration)),
|
|
222
|
+
[collection, tableConfiguration]
|
|
223
|
+
);
|
|
219
224
|
|
|
220
225
|
const table = useReactTable({
|
|
221
226
|
data: filteredData,
|
|
@@ -229,6 +234,9 @@ export const Table = <T extends TCollectionTypes>({
|
|
|
229
234
|
getPaginationRowModel: getPaginationRowModel(),
|
|
230
235
|
state: {
|
|
231
236
|
columnFilters,
|
|
237
|
+
columnVisibility: Object.fromEntries(
|
|
238
|
+
Object.entries(tableConfiguration?.columns ?? {}).map(([key, value]) => [key, value.visible ?? true])
|
|
239
|
+
),
|
|
232
240
|
},
|
|
233
241
|
});
|
|
234
242
|
|