@eventcatalog/core 3.25.5 → 3.26.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/catalog-to-astro-content-directory.cjs +1 -1
- package/dist/{chunk-NIGGP5OH.js → chunk-7CRFNX47.js} +1 -1
- package/dist/{chunk-G3KLCHZ7.js → chunk-ASC3AR2X.js} +1 -1
- package/dist/{chunk-KMBHKZX5.js → chunk-FQNBDDUF.js} +1 -1
- package/dist/{chunk-TQQ3EOI3.js → chunk-GCNIIIFG.js} +1 -1
- package/dist/{chunk-6QMOE3KE.js → chunk-XUN32ZVJ.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +563 -20
- package/dist/eventcatalog.js +572 -27
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/astro.config.mjs +2 -1
- package/eventcatalog/integrations/eventcatalog-features.ts +13 -0
- package/eventcatalog/public/icons/graphql.svg +3 -1
- package/eventcatalog/src/components/FieldsExplorer/FieldFilters.tsx +225 -0
- package/eventcatalog/src/components/FieldsExplorer/FieldNodeGraph.tsx +521 -0
- package/eventcatalog/src/components/FieldsExplorer/FieldsExplorer.tsx +501 -0
- package/eventcatalog/src/components/FieldsExplorer/FieldsTable.tsx +236 -0
- package/eventcatalog/src/enterprise/fields/field-extractor.test.ts +241 -0
- package/eventcatalog/src/enterprise/fields/field-extractor.ts +183 -0
- package/eventcatalog/src/enterprise/fields/field-indexer.ts +131 -0
- package/eventcatalog/src/enterprise/fields/fields-db.test.ts +186 -0
- package/eventcatalog/src/enterprise/fields/fields-db.ts +453 -0
- package/eventcatalog/src/enterprise/fields/pages/api/fields.ts +43 -0
- package/eventcatalog/src/enterprise/fields/pages/fields.astro +19 -0
- package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +23 -3
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +14 -16
- package/eventcatalog/src/utils/node-graphs/field-node-graph.ts +192 -0
- package/package.json +11 -9
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
import { ChevronLeft, ChevronRight, Lock, X } from 'lucide-react';
|
|
3
|
+
import FieldFilters from './FieldFilters';
|
|
4
|
+
import FieldsTable from './FieldsTable';
|
|
5
|
+
import type { FieldResult } from './FieldsTable';
|
|
6
|
+
import FieldNodeGraph from './FieldNodeGraph';
|
|
7
|
+
import { buildUrl } from '@utils/url-builder';
|
|
8
|
+
|
|
9
|
+
function DummyNode({
|
|
10
|
+
type,
|
|
11
|
+
name,
|
|
12
|
+
version,
|
|
13
|
+
className,
|
|
14
|
+
}: {
|
|
15
|
+
type: 'service' | 'event' | 'field';
|
|
16
|
+
name: string;
|
|
17
|
+
version?: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
}) {
|
|
20
|
+
const styles = {
|
|
21
|
+
service: { border: 'border-pink-500', badge: 'bg-pink-500', label: 'SERVICE' },
|
|
22
|
+
event: { border: 'border-orange-500', badge: 'bg-orange-500', label: 'EVENT' },
|
|
23
|
+
field: { border: 'border-cyan-500', badge: 'bg-cyan-500', label: 'FIELD' },
|
|
24
|
+
};
|
|
25
|
+
const s = styles[type];
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
className={`relative rounded-xl border-2 ${s.border} bg-[rgb(var(--ec-card-bg))] px-3 pt-4 pb-2.5 min-w-[140px] ${className || ''}`}
|
|
29
|
+
>
|
|
30
|
+
<div
|
|
31
|
+
className={`absolute -top-2.5 left-2.5 ${s.badge} text-white text-[7px] font-bold uppercase tracking-widest px-1.5 py-0.5 rounded`}
|
|
32
|
+
>
|
|
33
|
+
{s.label}
|
|
34
|
+
</div>
|
|
35
|
+
<div className="text-[11px] font-semibold text-[rgb(var(--ec-page-text))]">{name}</div>
|
|
36
|
+
{version && <div className="text-[9px] text-[rgb(var(--ec-page-text-muted))]">v{version}</div>}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function DummyEdge({ label }: { label: string }) {
|
|
42
|
+
return (
|
|
43
|
+
<div className="flex items-center gap-1">
|
|
44
|
+
<div className="w-8 h-px bg-[rgb(var(--ec-page-border))]" />
|
|
45
|
+
<span className="text-[8px] text-[rgb(var(--ec-page-text-muted))] whitespace-nowrap">{label}</span>
|
|
46
|
+
<div className="w-8 h-px bg-[rgb(var(--ec-page-border))]" />
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function FieldLineageUpgradeModal({ fieldPath, onClose }: { fieldPath: string; onClose: () => void }) {
|
|
52
|
+
return (
|
|
53
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
|
|
54
|
+
<div
|
|
55
|
+
className="w-[90vw] h-[80vh] max-w-[1400px] rounded-xl border shadow-2xl flex flex-col overflow-hidden relative"
|
|
56
|
+
style={{
|
|
57
|
+
backgroundColor: 'rgb(var(--ec-page-bg))',
|
|
58
|
+
borderColor: 'rgb(var(--ec-page-border))',
|
|
59
|
+
}}
|
|
60
|
+
onClick={(e) => e.stopPropagation()}
|
|
61
|
+
>
|
|
62
|
+
{/* Header */}
|
|
63
|
+
<div
|
|
64
|
+
className="flex items-center justify-between px-5 py-3 border-b flex-shrink-0"
|
|
65
|
+
style={{ borderColor: 'rgb(var(--ec-page-border))' }}
|
|
66
|
+
>
|
|
67
|
+
<div className="flex items-center gap-2">
|
|
68
|
+
<h3 className="text-sm font-semibold" style={{ color: 'rgb(var(--ec-page-text))' }}>
|
|
69
|
+
Field Traceability
|
|
70
|
+
</h3>
|
|
71
|
+
<code
|
|
72
|
+
className="px-2 py-0.5 rounded text-xs font-mono"
|
|
73
|
+
style={{
|
|
74
|
+
backgroundColor: 'rgb(var(--ec-accent) / 0.1)',
|
|
75
|
+
color: 'rgb(var(--ec-accent))',
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
{fieldPath}
|
|
79
|
+
</code>
|
|
80
|
+
</div>
|
|
81
|
+
<button
|
|
82
|
+
onClick={onClose}
|
|
83
|
+
className="p-1.5 rounded-lg transition-colors hover:bg-[rgb(var(--ec-content-hover))] flex-shrink-0"
|
|
84
|
+
style={{ color: 'rgb(var(--ec-icon-color))' }}
|
|
85
|
+
>
|
|
86
|
+
<X className="w-4 h-4" />
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{/* Blurred dummy content */}
|
|
91
|
+
<div className="flex-1 relative overflow-hidden">
|
|
92
|
+
{/* Fake graph + side panel */}
|
|
93
|
+
<div className="absolute inset-0 flex blur-[2px] opacity-60 select-none pointer-events-none">
|
|
94
|
+
{/* Fake graph area */}
|
|
95
|
+
<div className="flex-1 flex items-center justify-center p-8">
|
|
96
|
+
<div className="flex items-center gap-3">
|
|
97
|
+
{/* Producer services */}
|
|
98
|
+
<div className="flex flex-col gap-6">
|
|
99
|
+
<DummyNode type="service" name="Billing Service" version="1.2.0" />
|
|
100
|
+
<DummyNode type="service" name="Order Service" version="2.0.1" />
|
|
101
|
+
</div>
|
|
102
|
+
{/* Edges to events */}
|
|
103
|
+
<div className="flex flex-col gap-12">
|
|
104
|
+
<DummyEdge label="produces" />
|
|
105
|
+
<DummyEdge label="produces" />
|
|
106
|
+
</div>
|
|
107
|
+
{/* Events */}
|
|
108
|
+
<div className="flex flex-col gap-6">
|
|
109
|
+
<DummyNode type="event" name="Payment Due" version="0.0.1" />
|
|
110
|
+
<DummyNode type="event" name="Order Created" version="1.0.0" />
|
|
111
|
+
</div>
|
|
112
|
+
{/* Edges to field */}
|
|
113
|
+
<div className="flex flex-col gap-12">
|
|
114
|
+
<DummyEdge label="contains" />
|
|
115
|
+
<DummyEdge label="contains" />
|
|
116
|
+
</div>
|
|
117
|
+
{/* Field */}
|
|
118
|
+
<div className="flex flex-col gap-6">
|
|
119
|
+
<DummyNode type="field" name={fieldPath} />
|
|
120
|
+
</div>
|
|
121
|
+
{/* Edges to consumers */}
|
|
122
|
+
<div className="flex flex-col gap-12">
|
|
123
|
+
<DummyEdge label="consumed by" />
|
|
124
|
+
</div>
|
|
125
|
+
{/* Consumer services */}
|
|
126
|
+
<div className="flex flex-col gap-6">
|
|
127
|
+
<DummyNode type="service" name="Notification Service" version="0.0.2" />
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
{/* Fake side panel */}
|
|
132
|
+
<div className="w-[280px] flex-shrink-0 border-l border-[rgb(var(--ec-page-border))] p-4 space-y-4">
|
|
133
|
+
<div>
|
|
134
|
+
<div className="text-[11px] font-medium text-[rgb(var(--ec-page-text-muted))] uppercase tracking-wider mb-2">
|
|
135
|
+
Field
|
|
136
|
+
</div>
|
|
137
|
+
<div className="text-sm font-mono font-semibold text-[rgb(var(--ec-page-text))]">{fieldPath}</div>
|
|
138
|
+
<div className="text-xs text-[rgb(var(--ec-page-text-muted))] mt-1">string</div>
|
|
139
|
+
</div>
|
|
140
|
+
<div>
|
|
141
|
+
<div className="text-[11px] font-medium text-amber-500 uppercase tracking-wider mb-2">Type Conflict</div>
|
|
142
|
+
<div className="rounded-lg border border-amber-500/20 bg-amber-500/5 p-2.5 space-y-1">
|
|
143
|
+
<div className="text-xs text-amber-600">Inconsistent types detected</div>
|
|
144
|
+
<div className="flex justify-between text-xs">
|
|
145
|
+
<code className="font-mono">string</code>
|
|
146
|
+
<span className="text-[rgb(var(--ec-page-text-muted))]">3 schemas</span>
|
|
147
|
+
</div>
|
|
148
|
+
<div className="flex justify-between text-xs">
|
|
149
|
+
<code className="font-mono">integer</code>
|
|
150
|
+
<span className="text-[rgb(var(--ec-page-text-muted))]">1 schema</span>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
<div>
|
|
155
|
+
<div className="text-[11px] font-medium text-[rgb(var(--ec-page-text-muted))] uppercase tracking-wider mb-2">
|
|
156
|
+
Messages (3)
|
|
157
|
+
</div>
|
|
158
|
+
<div className="space-y-1.5">
|
|
159
|
+
<div className="rounded-lg border border-[rgb(var(--ec-page-border)/0.5)] bg-[rgb(var(--ec-card-bg))] px-2.5 py-2 text-xs">
|
|
160
|
+
Payment Due
|
|
161
|
+
</div>
|
|
162
|
+
<div className="rounded-lg border border-[rgb(var(--ec-page-border)/0.5)] bg-[rgb(var(--ec-card-bg))] px-2.5 py-2 text-xs">
|
|
163
|
+
Order Created
|
|
164
|
+
</div>
|
|
165
|
+
<div className="rounded-lg border border-[rgb(var(--ec-page-border)/0.5)] bg-[rgb(var(--ec-card-bg))] px-2.5 py-2 text-xs">
|
|
166
|
+
Invoice Generated
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{/* Upgrade prompt overlay */}
|
|
174
|
+
<div className="absolute inset-0 flex items-center justify-center bg-[rgb(var(--ec-page-bg)/0.5)]">
|
|
175
|
+
<div className="text-center max-w-lg px-6 py-8 rounded-2xl border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-page-bg))] shadow-2xl">
|
|
176
|
+
<div className="w-12 h-12 rounded-full bg-[rgb(var(--ec-accent)/0.1)] flex items-center justify-center mx-auto mb-4">
|
|
177
|
+
<Lock className="w-6 h-6 text-[rgb(var(--ec-accent))]" />
|
|
178
|
+
</div>
|
|
179
|
+
<h3 className="text-lg font-semibold text-[rgb(var(--ec-page-text))] mb-2">Field Lineage & Traceability</h3>
|
|
180
|
+
<p className="text-sm text-[rgb(var(--ec-page-text-muted))] mb-5 leading-relaxed">
|
|
181
|
+
See which services produce and consume this field, detect type conflicts across schemas, and trace field usage
|
|
182
|
+
across your entire event-driven architecture.
|
|
183
|
+
</p>
|
|
184
|
+
<a
|
|
185
|
+
href="https://eventcatalog.cloud/"
|
|
186
|
+
target="_blank"
|
|
187
|
+
rel="noopener noreferrer"
|
|
188
|
+
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-medium text-white bg-[rgb(var(--ec-accent))] hover:opacity-90 transition-opacity"
|
|
189
|
+
>
|
|
190
|
+
Start your 14-day free trial
|
|
191
|
+
</a>
|
|
192
|
+
<p className="text-xs text-[rgb(var(--ec-page-text-muted))] mt-3">
|
|
193
|
+
Available with EventCatalog Scale. No credit card required.
|
|
194
|
+
</p>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
interface Facets {
|
|
204
|
+
formats: { value: string; count: number }[];
|
|
205
|
+
types: { value: string; count: number }[];
|
|
206
|
+
messageTypes: { value: string; count: number }[];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
interface FieldsApiResponse {
|
|
210
|
+
fields: FieldResult[];
|
|
211
|
+
total: number;
|
|
212
|
+
cursor: string | null;
|
|
213
|
+
facets: Facets;
|
|
214
|
+
error?: string;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
interface SelectedField {
|
|
218
|
+
path: string;
|
|
219
|
+
type: string;
|
|
220
|
+
description: string;
|
|
221
|
+
required: boolean;
|
|
222
|
+
conflicts?: { type: string; count: number }[];
|
|
223
|
+
occurrences: FieldResult[];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
interface FieldsExplorerProps {
|
|
227
|
+
isScaleEnabled?: boolean;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export default function FieldsExplorer({ isScaleEnabled = false }: FieldsExplorerProps) {
|
|
231
|
+
const [fields, setFields] = useState<FieldResult[]>([]);
|
|
232
|
+
const [total, setTotal] = useState(0);
|
|
233
|
+
const [cursor, setCursor] = useState<string | null>(null);
|
|
234
|
+
const [facets, setFacets] = useState<Facets | null>(null);
|
|
235
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
236
|
+
const [error, setError] = useState<string | null>(null);
|
|
237
|
+
|
|
238
|
+
// Filter state
|
|
239
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
240
|
+
const [selectedFormats, setSelectedFormats] = useState<string[]>([]);
|
|
241
|
+
const [selectedMessageTypes, setSelectedMessageTypes] = useState<string[]>([]);
|
|
242
|
+
const [sharedOnly, setSharedOnly] = useState(false);
|
|
243
|
+
const [conflictingOnly, setConflictingOnly] = useState(false);
|
|
244
|
+
|
|
245
|
+
// Modal state
|
|
246
|
+
const [selectedField, setSelectedField] = useState<SelectedField | null>(null);
|
|
247
|
+
|
|
248
|
+
// Pagination cursors
|
|
249
|
+
const [cursorHistory, setCursorHistory] = useState<string[]>([]);
|
|
250
|
+
const [currentPageIndex, setCurrentPageIndex] = useState(0);
|
|
251
|
+
|
|
252
|
+
// Abort controller for in-flight requests
|
|
253
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
254
|
+
|
|
255
|
+
const buildQueryParams = useCallback(
|
|
256
|
+
(extraParams?: Record<string, string>) => {
|
|
257
|
+
const params = new URLSearchParams();
|
|
258
|
+
if (searchQuery) params.set('q', searchQuery);
|
|
259
|
+
if (selectedFormats.length > 0) params.set('format', selectedFormats.join(','));
|
|
260
|
+
if (selectedMessageTypes.length > 0) params.set('messageType', selectedMessageTypes.join(','));
|
|
261
|
+
if (sharedOnly) params.set('shared', 'true');
|
|
262
|
+
if (conflictingOnly) params.set('conflicting', 'true');
|
|
263
|
+
if (extraParams) {
|
|
264
|
+
Object.entries(extraParams).forEach(([key, value]) => {
|
|
265
|
+
params.set(key, value);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return params;
|
|
269
|
+
},
|
|
270
|
+
[searchQuery, selectedFormats, selectedMessageTypes, sharedOnly, conflictingOnly]
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
const fetchFields = useCallback(
|
|
274
|
+
async (cursorValue?: string) => {
|
|
275
|
+
// Cancel any in-flight request
|
|
276
|
+
if (abortControllerRef.current) {
|
|
277
|
+
abortControllerRef.current.abort();
|
|
278
|
+
}
|
|
279
|
+
const controller = new AbortController();
|
|
280
|
+
abortControllerRef.current = controller;
|
|
281
|
+
|
|
282
|
+
setIsLoading(true);
|
|
283
|
+
setError(null);
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const params = buildQueryParams(cursorValue ? { cursor: cursorValue } : undefined);
|
|
287
|
+
const response = await fetch(buildUrl(`/api/schemas/fields?${params.toString()}`), {
|
|
288
|
+
signal: controller.signal,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
if (!response.ok) {
|
|
292
|
+
const data = await response.json();
|
|
293
|
+
throw new Error(data.error || `HTTP ${response.status}`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const data: FieldsApiResponse = await response.json();
|
|
297
|
+
setFields(data.fields || []);
|
|
298
|
+
setTotal(data.total || 0);
|
|
299
|
+
setCursor(data.cursor || null);
|
|
300
|
+
if (data.facets) {
|
|
301
|
+
setFacets({
|
|
302
|
+
formats: data.facets.formats || [],
|
|
303
|
+
types: data.facets.types || [],
|
|
304
|
+
messageTypes: data.facets.messageTypes || [],
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
} catch (err: any) {
|
|
308
|
+
if (err.name === 'AbortError') return;
|
|
309
|
+
setError(err.message || 'Failed to load fields');
|
|
310
|
+
} finally {
|
|
311
|
+
if (!controller.signal.aborted) {
|
|
312
|
+
setIsLoading(false);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
[buildQueryParams]
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
// Initial fetch and re-fetch when filters change
|
|
320
|
+
useEffect(() => {
|
|
321
|
+
setCursorHistory([]);
|
|
322
|
+
setCurrentPageIndex(0);
|
|
323
|
+
fetchFields();
|
|
324
|
+
}, [searchQuery, selectedFormats, selectedMessageTypes, sharedOnly, conflictingOnly]);
|
|
325
|
+
|
|
326
|
+
// Open modal: fetch all occurrences for the field path, then show modal
|
|
327
|
+
const handleSelectField = useCallback(async (fieldPath: string) => {
|
|
328
|
+
// Non-scale users get the locked upgrade modal
|
|
329
|
+
if (!isScaleEnabled) {
|
|
330
|
+
setSelectedField({
|
|
331
|
+
path: fieldPath,
|
|
332
|
+
type: 'unknown',
|
|
333
|
+
description: '',
|
|
334
|
+
required: false,
|
|
335
|
+
occurrences: [],
|
|
336
|
+
});
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
// Fetch all pages of occurrences for this field path
|
|
342
|
+
let allOccurrences: FieldResult[] = [];
|
|
343
|
+
let nextCursor: string | null = null;
|
|
344
|
+
|
|
345
|
+
do {
|
|
346
|
+
const params = new URLSearchParams();
|
|
347
|
+
params.set('path', fieldPath);
|
|
348
|
+
params.set('pageSize', '100');
|
|
349
|
+
if (nextCursor) params.set('cursor', nextCursor);
|
|
350
|
+
|
|
351
|
+
const response = await fetch(buildUrl(`/api/schemas/fields?${params.toString()}`));
|
|
352
|
+
if (!response.ok) {
|
|
353
|
+
throw new Error(`HTTP ${response.status}`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const data: FieldsApiResponse = await response.json();
|
|
357
|
+
allOccurrences = allOccurrences.concat(data.fields || []);
|
|
358
|
+
nextCursor = data.cursor;
|
|
359
|
+
} while (nextCursor);
|
|
360
|
+
|
|
361
|
+
setSelectedField({
|
|
362
|
+
path: fieldPath,
|
|
363
|
+
type: allOccurrences[0]?.type || 'unknown',
|
|
364
|
+
description: allOccurrences[0]?.description || '',
|
|
365
|
+
required: allOccurrences[0]?.required || false,
|
|
366
|
+
conflicts: allOccurrences[0]?.conflicts,
|
|
367
|
+
occurrences: allOccurrences,
|
|
368
|
+
});
|
|
369
|
+
} catch {
|
|
370
|
+
// Silently fail — user can retry by clicking again
|
|
371
|
+
}
|
|
372
|
+
}, []);
|
|
373
|
+
|
|
374
|
+
const handleNextPage = useCallback(() => {
|
|
375
|
+
if (!cursor) return;
|
|
376
|
+
setCursorHistory((prev) => [...prev, cursor]);
|
|
377
|
+
setCurrentPageIndex((prev) => prev + 1);
|
|
378
|
+
fetchFields(cursor);
|
|
379
|
+
}, [cursor, fetchFields]);
|
|
380
|
+
|
|
381
|
+
const handlePrevPage = useCallback(() => {
|
|
382
|
+
if (currentPageIndex === 0) return;
|
|
383
|
+
const newIndex = currentPageIndex - 1;
|
|
384
|
+
setCurrentPageIndex(newIndex);
|
|
385
|
+
if (newIndex === 0) {
|
|
386
|
+
fetchFields();
|
|
387
|
+
} else {
|
|
388
|
+
fetchFields(cursorHistory[newIndex - 1]);
|
|
389
|
+
}
|
|
390
|
+
setCursorHistory((prev) => prev.slice(0, -1));
|
|
391
|
+
}, [currentPageIndex, cursorHistory, fetchFields]);
|
|
392
|
+
|
|
393
|
+
// Map facets for FieldFilters
|
|
394
|
+
const filterFacets = facets
|
|
395
|
+
? {
|
|
396
|
+
formats: facets.formats,
|
|
397
|
+
messageTypes: facets.messageTypes,
|
|
398
|
+
}
|
|
399
|
+
: null;
|
|
400
|
+
|
|
401
|
+
return (
|
|
402
|
+
<div className="flex h-full">
|
|
403
|
+
{/* Filter Sidebar */}
|
|
404
|
+
<div className="w-[320px] flex-shrink-0 flex flex-col bg-[rgb(var(--ec-page-bg))] bg-gradient-to-bl from-[rgb(var(--ec-page-bg))] via-[rgb(var(--ec-page-bg))] to-[rgb(var(--ec-accent)/0.08)] border-r border-[rgb(var(--ec-page-border))]">
|
|
405
|
+
<div className="flex-1 overflow-y-auto px-4 pt-4 pb-4">
|
|
406
|
+
<FieldFilters
|
|
407
|
+
searchQuery={searchQuery}
|
|
408
|
+
onSearchChange={setSearchQuery}
|
|
409
|
+
selectedFormats={selectedFormats}
|
|
410
|
+
onFormatsChange={setSelectedFormats}
|
|
411
|
+
selectedMessageTypes={selectedMessageTypes}
|
|
412
|
+
onMessageTypesChange={setSelectedMessageTypes}
|
|
413
|
+
sharedOnly={sharedOnly}
|
|
414
|
+
onSharedOnlyChange={setSharedOnly}
|
|
415
|
+
conflictingOnly={conflictingOnly}
|
|
416
|
+
onConflictingOnlyChange={setConflictingOnly}
|
|
417
|
+
facets={filterFacets}
|
|
418
|
+
isScaleEnabled={isScaleEnabled}
|
|
419
|
+
/>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
{/* Main Content */}
|
|
424
|
+
<div className="flex-1 min-w-0 flex flex-col overflow-hidden">
|
|
425
|
+
{/* Header */}
|
|
426
|
+
<div className="flex items-center justify-between px-6 py-4">
|
|
427
|
+
<h2 className="text-lg font-semibold text-[rgb(var(--ec-page-text))]">
|
|
428
|
+
Fields <span className="text-sm text-[rgb(var(--ec-page-text-muted))] font-normal ml-1">({total})</span>
|
|
429
|
+
</h2>
|
|
430
|
+
</div>
|
|
431
|
+
|
|
432
|
+
{/* Error state */}
|
|
433
|
+
{error && (
|
|
434
|
+
<div className="mx-6 mb-4 px-4 py-3 rounded-lg border border-red-300 bg-red-50 text-red-700 text-sm">{error}</div>
|
|
435
|
+
)}
|
|
436
|
+
|
|
437
|
+
{/* Table */}
|
|
438
|
+
<FieldsTable fields={fields} onSelectField={handleSelectField} isLoading={isLoading} isScaleEnabled={isScaleEnabled} />
|
|
439
|
+
|
|
440
|
+
{/* Pagination */}
|
|
441
|
+
<div className="flex-shrink-0 flex items-center justify-between px-6 py-3 border-t border-[rgb(var(--ec-page-border))]">
|
|
442
|
+
<span className="text-xs text-[rgb(var(--ec-page-text-muted))]">
|
|
443
|
+
{total > 0 && (
|
|
444
|
+
<>
|
|
445
|
+
<span className="font-medium text-[rgb(var(--ec-page-text))]">{fields.length}</span> of{' '}
|
|
446
|
+
<span className="font-medium text-[rgb(var(--ec-page-text))]">{total}</span> fields
|
|
447
|
+
</>
|
|
448
|
+
)}
|
|
449
|
+
</span>
|
|
450
|
+
<div className="flex items-center gap-1.5">
|
|
451
|
+
<button
|
|
452
|
+
className="p-1.5 text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-page-text))] disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
|
453
|
+
onClick={handlePrevPage}
|
|
454
|
+
disabled={currentPageIndex === 0}
|
|
455
|
+
title="Previous page"
|
|
456
|
+
>
|
|
457
|
+
<ChevronLeft className="w-4 h-4" />
|
|
458
|
+
</button>
|
|
459
|
+
<span className="text-xs tabular-nums text-[rgb(var(--ec-page-text-muted))] min-w-[40px] text-center">
|
|
460
|
+
<span className="font-medium text-[rgb(var(--ec-page-text))]">{currentPageIndex + 1}</span>
|
|
461
|
+
</span>
|
|
462
|
+
<button
|
|
463
|
+
className="p-1.5 text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-page-text))] disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
|
464
|
+
onClick={handleNextPage}
|
|
465
|
+
disabled={!cursor}
|
|
466
|
+
title="Next page"
|
|
467
|
+
>
|
|
468
|
+
<ChevronRight className="w-4 h-4" />
|
|
469
|
+
</button>
|
|
470
|
+
</div>
|
|
471
|
+
</div>
|
|
472
|
+
</div>
|
|
473
|
+
|
|
474
|
+
{/* Field Detail Modal with Node Graph */}
|
|
475
|
+
{selectedField && !isScaleEnabled && (
|
|
476
|
+
<FieldLineageUpgradeModal fieldPath={selectedField.path} onClose={() => setSelectedField(null)} />
|
|
477
|
+
)}
|
|
478
|
+
{selectedField && isScaleEnabled && (
|
|
479
|
+
<FieldNodeGraph
|
|
480
|
+
fieldPath={selectedField.path}
|
|
481
|
+
fieldType={selectedField.type}
|
|
482
|
+
fieldDescription={selectedField.description}
|
|
483
|
+
fieldRequired={selectedField.required}
|
|
484
|
+
fieldConflicts={selectedField.conflicts}
|
|
485
|
+
occurrences={selectedField.occurrences.map((f) => ({
|
|
486
|
+
messageId: f.messageId,
|
|
487
|
+
messageVersion: f.messageVersion,
|
|
488
|
+
messageType: f.messageType,
|
|
489
|
+
messageName: f.messageName,
|
|
490
|
+
messageSummary: f.messageSummary,
|
|
491
|
+
messageOwners: f.messageOwners,
|
|
492
|
+
fieldType: f.type,
|
|
493
|
+
producers: f.producers,
|
|
494
|
+
consumers: f.consumers,
|
|
495
|
+
}))}
|
|
496
|
+
onClose={() => setSelectedField(null)}
|
|
497
|
+
/>
|
|
498
|
+
)}
|
|
499
|
+
</div>
|
|
500
|
+
);
|
|
501
|
+
}
|