@atolis-hq/corum 0.1.0 → 0.1.5
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/README.md +220 -223
- package/dist/src/bin/corum.js +39 -39
- package/package.json +40 -36
- package/web/app.jsx +668 -668
- package/web/index.html +41 -41
- package/web/nav.js +141 -141
- package/web/primitives.jsx +583 -583
- package/web/router.js +49 -49
- package/web/style.css +827 -827
- package/dist/src/cli.js +0 -20
- package/dist/src/openapi-to-api-endpoints.js +0 -240
package/web/primitives.jsx
CHANGED
|
@@ -1,583 +1,583 @@
|
|
|
1
|
-
/* Shared UI primitives for the browser app. */
|
|
2
|
-
|
|
3
|
-
function navigate(path) {
|
|
4
|
-
window.location.hash = path;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
function BrandMark({ size = 24, color = 'currentColor' }) {
|
|
8
|
-
return (
|
|
9
|
-
<svg width={size} height={size} viewBox="0 0 110 110" aria-hidden="true">
|
|
10
|
-
<g transform="translate(55 55)" stroke={color} fill={color} strokeLinecap="round">
|
|
11
|
-
<path d="M43.8 3.8A44 44 0 0 1 38.1 22" fill="none" strokeWidth="7.5" opacity="0.55" />
|
|
12
|
-
<path d="M38.1 22a44 44 0 1 1-19.5-61" fill="none" strokeWidth="7.5" />
|
|
13
|
-
<circle r="3" opacity="0.3" />
|
|
14
|
-
<circle r="1.8" />
|
|
15
|
-
<path strokeWidth="3" d="M0 0v-26" /><circle cy="-26" r="4.5" opacity="0.85" />
|
|
16
|
-
<path strokeWidth="3" d="m0 0 13-22" /><circle cx="13" cy="-22" r="3.5" opacity="0.78" />
|
|
17
|
-
<path strokeWidth="3" d="m0 0 23-9" /><circle cx="23" cy="-9" r="4" opacity="0.72" />
|
|
18
|
-
<path strokeWidth="2.5" opacity="0.7" d="m0 0 21 11" /><circle cx="21" cy="11" r="3" opacity="0.6" />
|
|
19
|
-
<path strokeWidth="2.5" opacity="0.55" d="m0 0 9 23" /><circle cx="9" cy="23" r="3.5" opacity="0.5" />
|
|
20
|
-
<path strokeWidth="2.5" opacity="0.55" d="m0 0-11 22" /><circle cx="-11" cy="22" r="3" opacity="0.48" />
|
|
21
|
-
<path strokeWidth="3" d="m0 0-26 3" /><circle cx="-26" cy="3" r="4" opacity="0.75" />
|
|
22
|
-
<path strokeWidth="3" d="m0 0-20-17" /><circle cx="-20" cy="-17" r="4.5" opacity="0.85" />
|
|
23
|
-
<path strokeWidth="2" opacity="0.66" d="m0 0-8-23" /><circle cx="-8" cy="-23" r="3" opacity="0.62" />
|
|
24
|
-
</g>
|
|
25
|
-
</svg>
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function Icon({ name, size }) {
|
|
30
|
-
const style = size ? { fontSize: size } : undefined;
|
|
31
|
-
return <i className={`fa-solid fa-${name}`} style={style} aria-hidden="true" />;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function StateTag({ state }) {
|
|
35
|
-
return <span className={`tag state-${state}`}>{state}</span>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function StabilityTag({ stability }) {
|
|
39
|
-
return <span className={`tag stability-${stability}`}>{stability}</span>;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function Chip({ children, title }) {
|
|
43
|
-
return <span className="chip" title={title}>{children}</span>;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function TemplateBadge({ name, colour }) {
|
|
47
|
-
const style = colour ? { background: colour } : { background: 'var(--ink-4)' };
|
|
48
|
-
return <span className="template-badge" style={style}>{name}</span>;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function PropertyValue({ value, onNavigate }) {
|
|
52
|
-
if (value === null || value === undefined) return <span className="prop-empty">-</span>;
|
|
53
|
-
|
|
54
|
-
if (typeof value === 'object' && 'display' in value && 'nodeId' in value) {
|
|
55
|
-
return (
|
|
56
|
-
<a className="node-ref-link" onClick={() => onNavigate && onNavigate(value.nodeId)}>
|
|
57
|
-
{value.display}
|
|
58
|
-
</a>
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (typeof value === 'object' && 'display' in value) {
|
|
63
|
-
return <span>{value.display}</span>;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (Array.isArray(value)) {
|
|
67
|
-
return <span>{value.length === 1 ? '1 item' : `${value.length} items`}</span>;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (typeof value === 'object') {
|
|
71
|
-
return <span className="prop-empty">-</span>;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return <span>{String(value)}</span>;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function buildPropertyRows(entries, onNavigate, depth = 0, parentPath = '') {
|
|
78
|
-
return entries.flatMap(([key, value], index) => {
|
|
79
|
-
const rowPath = parentPath ? `${parentPath}.${key}` : key;
|
|
80
|
-
const rowKey = `${rowPath}:${index}`;
|
|
81
|
-
|
|
82
|
-
if (Array.isArray(value)) {
|
|
83
|
-
const rows = [{
|
|
84
|
-
key: rowKey,
|
|
85
|
-
label: key,
|
|
86
|
-
depth,
|
|
87
|
-
value: <PropertyValue value={value} onNavigate={onNavigate} />,
|
|
88
|
-
}];
|
|
89
|
-
|
|
90
|
-
value.forEach((item, itemIndex) => {
|
|
91
|
-
const itemPath = `${rowPath}[${itemIndex}]`;
|
|
92
|
-
if (item && typeof item === 'object' && !('display' in item) && !Array.isArray(item)) {
|
|
93
|
-
rows.push({
|
|
94
|
-
key: `${itemPath}:group`,
|
|
95
|
-
label: `[${itemIndex}]`,
|
|
96
|
-
depth: depth + 1,
|
|
97
|
-
value: <span className="prop-empty">-</span>,
|
|
98
|
-
});
|
|
99
|
-
rows.push(...buildPropertyRows(Object.entries(item), onNavigate, depth + 2, itemPath));
|
|
100
|
-
} else {
|
|
101
|
-
rows.push({
|
|
102
|
-
key: `${itemPath}:value`,
|
|
103
|
-
label: `[${itemIndex}]`,
|
|
104
|
-
depth: depth + 1,
|
|
105
|
-
value: <PropertyValue value={item} onNavigate={onNavigate} />,
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
return rows;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (value && typeof value === 'object' && !('display' in value)) {
|
|
114
|
-
return [
|
|
115
|
-
{
|
|
116
|
-
key: rowKey,
|
|
117
|
-
label: key,
|
|
118
|
-
depth,
|
|
119
|
-
value: <span className="prop-empty">-</span>,
|
|
120
|
-
},
|
|
121
|
-
...buildPropertyRows(Object.entries(value), onNavigate, depth + 1, rowPath),
|
|
122
|
-
];
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return [{
|
|
126
|
-
key: rowKey,
|
|
127
|
-
label: key,
|
|
128
|
-
depth,
|
|
129
|
-
value: <PropertyValue value={value} onNavigate={onNavigate} />,
|
|
130
|
-
}];
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function PropertiesTable({ properties, onNavigate }) {
|
|
135
|
-
const entries = Object.entries(properties ?? {});
|
|
136
|
-
const rows = buildPropertyRows(entries, onNavigate);
|
|
137
|
-
if (entries.length === 0) {
|
|
138
|
-
return <p className="label-sm" style={{ padding: '10px 14px' }}>No properties.</p>;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return (
|
|
142
|
-
<table className="prop-table">
|
|
143
|
-
<tbody>
|
|
144
|
-
{rows.map(row => (
|
|
145
|
-
<tr key={row.key} className={`prop-row${row.depth > 0 ? ' nested' : ''}`}>
|
|
146
|
-
<td className="mono prop-key-cell">
|
|
147
|
-
<span className="prop-key-label" style={{ '--prop-depth': row.depth }}>
|
|
148
|
-
{row.label}
|
|
149
|
-
</span>
|
|
150
|
-
</td>
|
|
151
|
-
<td>{row.value}</td>
|
|
152
|
-
</tr>
|
|
153
|
-
))}
|
|
154
|
-
</tbody>
|
|
155
|
-
</table>
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function localSchemaName(nodeId) {
|
|
160
|
-
const marker = '.schemas.';
|
|
161
|
-
const idx = nodeId.indexOf(marker);
|
|
162
|
-
if (idx < 0) return nodeId.split('.').pop();
|
|
163
|
-
return nodeId.slice(idx + marker.length).split('.')[0];
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function fieldSchemaName(nodeId) {
|
|
167
|
-
const schemaMarker = '.schemas.';
|
|
168
|
-
const fieldMarker = '.fields.';
|
|
169
|
-
const schemaIdx = nodeId.indexOf(schemaMarker);
|
|
170
|
-
const fieldIdx = nodeId.indexOf(fieldMarker);
|
|
171
|
-
if (schemaIdx < 0 || fieldIdx < 0) return null;
|
|
172
|
-
return nodeId.slice(schemaIdx + schemaMarker.length, fieldIdx);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function fieldLocalName(nodeId) {
|
|
176
|
-
const marker = '.fields.';
|
|
177
|
-
const idx = nodeId.indexOf(marker);
|
|
178
|
-
return idx < 0 ? nodeId.split('.').pop() : nodeId.slice(idx + marker.length);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function clusterNodeId(nodeId) {
|
|
182
|
-
const sectionMatch = nodeId.match(/\.(schemas|enums|operations)\./);
|
|
183
|
-
if (sectionMatch && sectionMatch.index !== undefined) {
|
|
184
|
-
return nodeId.slice(0, sectionMatch.index);
|
|
185
|
-
}
|
|
186
|
-
return nodeId.replace(/\.(fields|values)\.[^.]+$/, '');
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function refName(ref) {
|
|
190
|
-
if (typeof ref === 'string') return ref.replace(/^#\/(schemas|enums)\//, '');
|
|
191
|
-
if (ref && typeof ref === 'object' && 'display' in ref) return ref.display;
|
|
192
|
-
return String(ref);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function refLocalSchemaName(ref) {
|
|
196
|
-
if (typeof ref === 'string') return ref.startsWith('#/schemas/') ? ref.slice(10) : null;
|
|
197
|
-
if (ref && typeof ref === 'object' && 'display' in ref) return ref.display;
|
|
198
|
-
return null;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function fieldType(properties) {
|
|
202
|
-
const cardinality = properties?.cardinality === 'many' ? '[]' : '';
|
|
203
|
-
if (properties?.type) return `${properties.type}${cardinality}`;
|
|
204
|
-
const ref = properties?.['$ref'];
|
|
205
|
-
if (ref) return `${refName(ref)}${cardinality}`;
|
|
206
|
-
return cardinality ? `unknown${cardinality}` : 'unknown';
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function fieldRequirement(properties) {
|
|
210
|
-
if (properties?.nullable === false) return 'required';
|
|
211
|
-
if (properties?.nullable === true) return 'optional';
|
|
212
|
-
return '-';
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function fieldCardinality(properties) {
|
|
216
|
-
return properties?.cardinality ?? '-';
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function fieldDetails(properties) {
|
|
220
|
-
const parts = [];
|
|
221
|
-
const ref = properties?.['$ref'];
|
|
222
|
-
if (ref) parts.push(`ref ${refName(ref)}`);
|
|
223
|
-
if (properties?.description) parts.push(properties.description);
|
|
224
|
-
return parts.length > 0 ? parts.join(' · ') : '-';
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function linkSummary(edge, fieldId) {
|
|
228
|
-
const outgoing = edge.from === fieldId;
|
|
229
|
-
const otherNodeId = outgoing ? edge.to : edge.from;
|
|
230
|
-
const direction = outgoing ? '->' : '<-';
|
|
231
|
-
const relation = edge.type === 'maps-to' ? '' : `${edge.type} `;
|
|
232
|
-
return {
|
|
233
|
-
direction,
|
|
234
|
-
label: `${relation}${fieldLocalName(otherNodeId)}`,
|
|
235
|
-
targetNodeId: clusterNodeId(otherNodeId),
|
|
236
|
-
title: otherNodeId,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function localEnumName(nodeId) {
|
|
241
|
-
const marker = '.enums.';
|
|
242
|
-
const idx = nodeId.indexOf(marker);
|
|
243
|
-
if (idx < 0) return nodeId.split('.').pop();
|
|
244
|
-
return nodeId.slice(idx + marker.length).split('.')[0];
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function enumValueEnumName(nodeId) {
|
|
248
|
-
const enumMarker = '.enums.';
|
|
249
|
-
const valueMarker = '.values.';
|
|
250
|
-
const enumIdx = nodeId.indexOf(enumMarker);
|
|
251
|
-
const valueIdx = nodeId.indexOf(valueMarker);
|
|
252
|
-
if (enumIdx < 0 || valueIdx < 0) return null;
|
|
253
|
-
return nodeId.slice(enumIdx + enumMarker.length, valueIdx);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function enumValueDisplayName(node) {
|
|
257
|
-
return node.properties?.name ?? node.id.split('.').pop();
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function enumValueDescription(node) {
|
|
261
|
-
return node.properties?.description ?? '-';
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function buildSchemaModel(schemaNodes, allNodes) {
|
|
265
|
-
const schemasByName = new Map(schemaNodes.map(node => [localSchemaName(node.id), node]));
|
|
266
|
-
const fieldsBySchema = new Map();
|
|
267
|
-
const referencedSchemas = new Set();
|
|
268
|
-
|
|
269
|
-
for (const node of allNodes ?? []) {
|
|
270
|
-
if (node.template !== 'Field') continue;
|
|
271
|
-
const schemaName = fieldSchemaName(node.id);
|
|
272
|
-
if (!schemaName) continue;
|
|
273
|
-
if (!fieldsBySchema.has(schemaName)) fieldsBySchema.set(schemaName, []);
|
|
274
|
-
fieldsBySchema.get(schemaName).push(node);
|
|
275
|
-
|
|
276
|
-
const ref = node.properties?.['$ref'];
|
|
277
|
-
const localName = refLocalSchemaName(ref);
|
|
278
|
-
if (localName && schemasByName.has(localName)) {
|
|
279
|
-
referencedSchemas.add(localName);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const topSchemas = schemaNodes.filter(node => !referencedSchemas.has(localSchemaName(node.id)));
|
|
284
|
-
return {
|
|
285
|
-
schemasByName,
|
|
286
|
-
fieldsBySchema,
|
|
287
|
-
topSchemas: topSchemas.length > 0 ? topSchemas : schemaNodes,
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function SchemaFieldRows({ schemaName, model, prefix = '', depth = 0, visited = new Set(), edges = [], overlayFields, overlayRefs }) {
|
|
292
|
-
const fields = model.fieldsBySchema.get(schemaName) ?? [];
|
|
293
|
-
if (fields.length === 0) {
|
|
294
|
-
return (
|
|
295
|
-
<div className="field-row">
|
|
296
|
-
<div />
|
|
297
|
-
<div className="label-sm">No fields.</div>
|
|
298
|
-
<div />
|
|
299
|
-
<div />
|
|
300
|
-
<div />
|
|
301
|
-
<div />
|
|
302
|
-
<div />
|
|
303
|
-
</div>
|
|
304
|
-
);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
return (
|
|
308
|
-
<>
|
|
309
|
-
{fields.map(field => {
|
|
310
|
-
const name = fieldLocalName(field.id);
|
|
311
|
-
const ref = field.properties?.['$ref'];
|
|
312
|
-
const localRef = refLocalSchemaName(ref);
|
|
313
|
-
const canExpand = localRef !== null && model.schemasByName.has(localRef) && !visited.has(localRef);
|
|
314
|
-
const childSchemaNode = canExpand ? model.schemasByName.get(localRef) : null;
|
|
315
|
-
const childGhostFields = childSchemaNode ? overlayFieldsForSchema(overlayFields, childSchemaNode.id) : [];
|
|
316
|
-
const childPrefix = `${prefix}${name}${field.properties?.cardinality === 'many' ? '[].' : '.'}`;
|
|
317
|
-
const nextVisited = new Set(visited);
|
|
318
|
-
nextVisited.add(schemaName);
|
|
319
|
-
const links = edges.filter(e =>
|
|
320
|
-
(e.from === field.id || e.to === field.id)
|
|
321
|
-
&& e.type !== 'has-field'
|
|
322
|
-
&& e.type !== 'has-value',
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
return (
|
|
326
|
-
<React.Fragment key={field.id}>
|
|
327
|
-
<div className={`field-row${depth > 0 ? ' nested' : ''}`} style={{ '--field-depth': depth }}>
|
|
328
|
-
<div className="gutter">{canExpand && <Icon name="caret-down" size={11} />}</div>
|
|
329
|
-
<div className="name">{prefix}{name}</div>
|
|
330
|
-
<div className="type">{fieldType(field.properties)}</div>
|
|
331
|
-
<div className="cardinality">{fieldCardinality(field.properties)}</div>
|
|
332
|
-
<div className="req">{fieldRequirement(field.properties)}</div>
|
|
333
|
-
<div className="state"><StateTag state={field.state} /></div>
|
|
334
|
-
<div className="lineage">
|
|
335
|
-
{links.length > 0
|
|
336
|
-
? links.map((edge, index) => {
|
|
337
|
-
const link = linkSummary(edge, field.id);
|
|
338
|
-
return (
|
|
339
|
-
<React.Fragment key={edge.id}>
|
|
340
|
-
{index > 0 && <span key={`sep-${edge.id}`}>{' '}</span>}
|
|
341
|
-
<a
|
|
342
|
-
className="node-ref-link"
|
|
343
|
-
onClick={() => navigate(`/node?id=${encodeURIComponent(link.targetNodeId)}`)}
|
|
344
|
-
title={link.title}
|
|
345
|
-
>
|
|
346
|
-
{link.direction} {link.label}
|
|
347
|
-
</a>
|
|
348
|
-
</React.Fragment>
|
|
349
|
-
);
|
|
350
|
-
})
|
|
351
|
-
: fieldDetails(field.properties)}
|
|
352
|
-
</div>
|
|
353
|
-
</div>
|
|
354
|
-
{canExpand && (
|
|
355
|
-
<>
|
|
356
|
-
<SchemaFieldRows
|
|
357
|
-
schemaName={localRef}
|
|
358
|
-
model={model}
|
|
359
|
-
prefix={childPrefix}
|
|
360
|
-
depth={depth + 1}
|
|
361
|
-
visited={nextVisited}
|
|
362
|
-
edges={edges}
|
|
363
|
-
overlayFields={overlayFields}
|
|
364
|
-
overlayRefs={overlayRefs}
|
|
365
|
-
/>
|
|
366
|
-
{childGhostFields.length > 0 && (
|
|
367
|
-
<GhostFieldRows fields={childGhostFields} overlayRefs={overlayRefs} prefix={childPrefix} depth={depth + 1} />
|
|
368
|
-
)}
|
|
369
|
-
</>
|
|
370
|
-
)}
|
|
371
|
-
</React.Fragment>
|
|
372
|
-
);
|
|
373
|
-
})}
|
|
374
|
-
</>
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Max 2 distinct stripe colours — OverlayLegend also only renders 2 entries.
|
|
379
|
-
function ghostStripeClass(index) {
|
|
380
|
-
if (index === 0) return 'overlay-stripe-0';
|
|
381
|
-
if (index === 1) return 'overlay-stripe-1';
|
|
382
|
-
return 'overlay-stripe-0';
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function overlayFieldsForSchema(overlayFields, schemaNodeId) {
|
|
386
|
-
if (!overlayFields) return [];
|
|
387
|
-
const prefix = schemaNodeId + '.fields.';
|
|
388
|
-
return overlayFields.filter(field => field.id.startsWith(prefix));
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function GhostFieldRows({ fields, overlayRefs, prefix = '', depth = 0 }) {
|
|
392
|
-
return (
|
|
393
|
-
<>
|
|
394
|
-
{fields.map(field => {
|
|
395
|
-
const isConflict = field.ghostState === 'ghost-conflict' || field.ghostState === 'local-modified';
|
|
396
|
-
const refIndex = overlayRefs ? overlayRefs.indexOf(field.sourceRef) : 0;
|
|
397
|
-
const stripeClass = isConflict ? 'overlay-conflict' : ghostStripeClass(Math.max(0, refIndex));
|
|
398
|
-
const name = prefix + (fieldLocalName(field.id));
|
|
399
|
-
const type = field.node.properties?.type || (field.node.properties?.['$ref'] ? String(field.node.properties['$ref']).replace(/^#\/(schemas|enums)\//, '') : '-');
|
|
400
|
-
return (
|
|
401
|
-
<div
|
|
402
|
-
key={field.id}
|
|
403
|
-
className={`field-row overlay-ghost ${stripeClass}${depth > 0 ? ' nested' : ''}`}
|
|
404
|
-
style={{ '--field-depth': depth }}
|
|
405
|
-
title={`From ${field.sourceRef}`}
|
|
406
|
-
>
|
|
407
|
-
<div className="gutter">
|
|
408
|
-
{isConflict && <span style={{ color: '#c44', fontWeight: 700 }}>!</span>}
|
|
409
|
-
</div>
|
|
410
|
-
<div className="name">{name}</div>
|
|
411
|
-
<div className="type">{type}</div>
|
|
412
|
-
<div className="cardinality">-</div>
|
|
413
|
-
<div className="req">-</div>
|
|
414
|
-
<div className="state">
|
|
415
|
-
{isConflict
|
|
416
|
-
? <span className="tag" style={{ background: '#c4422222', color: '#c44' }}>conflict</span>
|
|
417
|
-
: <StateTag state={field.node.state} />
|
|
418
|
-
}
|
|
419
|
-
</div>
|
|
420
|
-
<div className="lineage">
|
|
421
|
-
<span className="mono" style={{ fontSize: 10.5, opacity: 0.7 }}>{field.sourceRef}</span>
|
|
422
|
-
</div>
|
|
423
|
-
</div>
|
|
424
|
-
);
|
|
425
|
-
})}
|
|
426
|
-
</>
|
|
427
|
-
);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function OverlayLegend({ overlayRefs }) {
|
|
431
|
-
if (!overlayRefs || overlayRefs.length === 0) return null;
|
|
432
|
-
const colours = ['var(--accent)', '#5c7aa8'];
|
|
433
|
-
return (
|
|
434
|
-
<div className="overlay-legend">
|
|
435
|
-
<span className="label-xs">Incoming:</span>
|
|
436
|
-
{overlayRefs.map((ref, index) => (
|
|
437
|
-
<span key={ref} className="overlay-legend-item">
|
|
438
|
-
<span
|
|
439
|
-
className="overlay-legend-swatch"
|
|
440
|
-
style={{ background: colours[index % colours.length] }}
|
|
441
|
-
/>
|
|
442
|
-
<span className="mono" style={{ fontSize: 10.5 }}>{ref}</span>
|
|
443
|
-
</span>
|
|
444
|
-
))}
|
|
445
|
-
<span className="overlay-legend-item" style={{ color: '#c44' }}>
|
|
446
|
-
<span className="overlay-legend-swatch" style={{ background: '#c44' }} />
|
|
447
|
-
conflict
|
|
448
|
-
</span>
|
|
449
|
-
</div>
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
function SchemaCard({ title, nodes, allNodes, edges, anchorIdForNode, overlayFields, overlayRefs }) {
|
|
454
|
-
if (!nodes || nodes.length === 0) return null;
|
|
455
|
-
|
|
456
|
-
if (title === 'EnumDefinition') {
|
|
457
|
-
const valuesByEnum = new Map();
|
|
458
|
-
for (const node of allNodes ?? []) {
|
|
459
|
-
if (node.template !== 'EnumValue') continue;
|
|
460
|
-
const enumName = enumValueEnumName(node.id);
|
|
461
|
-
if (!enumName) continue;
|
|
462
|
-
if (!valuesByEnum.has(enumName)) valuesByEnum.set(enumName, []);
|
|
463
|
-
valuesByEnum.get(enumName).push(node);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
return (
|
|
467
|
-
<div className="card enum-card">
|
|
468
|
-
<div className="card-head">Enums</div>
|
|
469
|
-
<div className="card-body">
|
|
470
|
-
{nodes.map(enumNode => {
|
|
471
|
-
const enumName = localEnumName(enumNode.id);
|
|
472
|
-
const values = valuesByEnum.get(enumName) ?? [];
|
|
473
|
-
return (
|
|
474
|
-
<div key={enumNode.id} className="enum-section" id={anchorIdForNode ? anchorIdForNode(enumNode.id) : undefined}>
|
|
475
|
-
<div className="schema-section-head">
|
|
476
|
-
<div>
|
|
477
|
-
<div className="schema-title">{enumName}</div>
|
|
478
|
-
{enumNode.properties?.description && <div className="label-sm">{enumNode.properties.description}</div>}
|
|
479
|
-
</div>
|
|
480
|
-
<div className="label-sm mono">{enumNode.id}</div>
|
|
481
|
-
</div>
|
|
482
|
-
<div className="enum-row enum-row-head">
|
|
483
|
-
<div className="label-xs">Name</div>
|
|
484
|
-
<div className="label-xs">Description</div>
|
|
485
|
-
<div className="label-xs">Status</div>
|
|
486
|
-
</div>
|
|
487
|
-
{values.length === 0 ? (
|
|
488
|
-
<div className="enum-row">
|
|
489
|
-
<div className="label-sm">No values.</div>
|
|
490
|
-
<div />
|
|
491
|
-
<div />
|
|
492
|
-
</div>
|
|
493
|
-
) : values.map(value => (
|
|
494
|
-
<div key={value.id} className="enum-row">
|
|
495
|
-
<div className="name">{enumValueDisplayName(value)}</div>
|
|
496
|
-
<div className="description">{enumValueDescription(value)}</div>
|
|
497
|
-
<div className="state"><StateTag state={value.state} /></div>
|
|
498
|
-
</div>
|
|
499
|
-
))}
|
|
500
|
-
</div>
|
|
501
|
-
);
|
|
502
|
-
})}
|
|
503
|
-
</div>
|
|
504
|
-
</div>
|
|
505
|
-
);
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
if (title === 'Schema') {
|
|
509
|
-
const model = buildSchemaModel(nodes, allNodes ?? nodes);
|
|
510
|
-
return (
|
|
511
|
-
<div className="card schema-card">
|
|
512
|
-
<div className="card-head">Schemas</div>
|
|
513
|
-
<div className="card-body">
|
|
514
|
-
{model.topSchemas.map(schema => {
|
|
515
|
-
const schemaName = localSchemaName(schema.id);
|
|
516
|
-
const ghostFields = overlayFieldsForSchema(overlayFields, schema.id);
|
|
517
|
-
return (
|
|
518
|
-
<div key={schema.id} className="schema-section" id={anchorIdForNode ? anchorIdForNode(schema.id) : undefined}>
|
|
519
|
-
<div className="schema-section-head">
|
|
520
|
-
<div>
|
|
521
|
-
<div className="schema-title">{schemaName}</div>
|
|
522
|
-
{schema.properties?.description && <div className="label-sm">{schema.properties.description}</div>}
|
|
523
|
-
</div>
|
|
524
|
-
<div className="label-sm mono">{schema.id}</div>
|
|
525
|
-
</div>
|
|
526
|
-
{ghostFields.length > 0 && <OverlayLegend overlayRefs={overlayRefs} />}
|
|
527
|
-
<div className="field-row field-row-head">
|
|
528
|
-
<div />
|
|
529
|
-
<div className="label-xs">Name</div>
|
|
530
|
-
<div className="label-xs">Type</div>
|
|
531
|
-
<div className="label-xs">Cardinality</div>
|
|
532
|
-
<div className="label-xs">Req</div>
|
|
533
|
-
<div className="label-xs">State</div>
|
|
534
|
-
<div className="label-xs">Links</div>
|
|
535
|
-
</div>
|
|
536
|
-
<SchemaFieldRows
|
|
537
|
-
schemaName={schemaName}
|
|
538
|
-
model={model}
|
|
539
|
-
visited={new Set()}
|
|
540
|
-
edges={edges ?? []}
|
|
541
|
-
overlayFields={overlayFields}
|
|
542
|
-
overlayRefs={overlayRefs}
|
|
543
|
-
/>
|
|
544
|
-
{ghostFields.length > 0 && (
|
|
545
|
-
<GhostFieldRows fields={ghostFields} overlayRefs={overlayRefs} />
|
|
546
|
-
)}
|
|
547
|
-
</div>
|
|
548
|
-
);
|
|
549
|
-
})}
|
|
550
|
-
</div>
|
|
551
|
-
</div>
|
|
552
|
-
);
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
return (
|
|
556
|
-
<div className="card">
|
|
557
|
-
<div className="card-head">{title}</div>
|
|
558
|
-
<div className="card-body">
|
|
559
|
-
{nodes.map(node => (
|
|
560
|
-
<div key={node.id} id={anchorIdForNode ? anchorIdForNode(node.id) : undefined} style={{ borderBottom: '1px dashed var(--rule)' }}>
|
|
561
|
-
<div className="mono" style={{ padding: '8px 14px 0', color: 'var(--ink-3)', fontSize: 11, fontWeight: 600 }}>
|
|
562
|
-
{node.id.split('.').pop()}
|
|
563
|
-
</div>
|
|
564
|
-
<PropertiesTable properties={node.properties} />
|
|
565
|
-
</div>
|
|
566
|
-
))}
|
|
567
|
-
</div>
|
|
568
|
-
</div>
|
|
569
|
-
);
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
window.CorumPrimitives = {
|
|
573
|
-
navigate,
|
|
574
|
-
BrandMark,
|
|
575
|
-
Icon,
|
|
576
|
-
StateTag,
|
|
577
|
-
StabilityTag,
|
|
578
|
-
Chip,
|
|
579
|
-
TemplateBadge,
|
|
580
|
-
PropertyValue,
|
|
581
|
-
PropertiesTable,
|
|
582
|
-
SchemaCard,
|
|
583
|
-
};
|
|
1
|
+
/* Shared UI primitives for the browser app. */
|
|
2
|
+
|
|
3
|
+
function navigate(path) {
|
|
4
|
+
window.location.hash = path;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function BrandMark({ size = 24, color = 'currentColor' }) {
|
|
8
|
+
return (
|
|
9
|
+
<svg width={size} height={size} viewBox="0 0 110 110" aria-hidden="true">
|
|
10
|
+
<g transform="translate(55 55)" stroke={color} fill={color} strokeLinecap="round">
|
|
11
|
+
<path d="M43.8 3.8A44 44 0 0 1 38.1 22" fill="none" strokeWidth="7.5" opacity="0.55" />
|
|
12
|
+
<path d="M38.1 22a44 44 0 1 1-19.5-61" fill="none" strokeWidth="7.5" />
|
|
13
|
+
<circle r="3" opacity="0.3" />
|
|
14
|
+
<circle r="1.8" />
|
|
15
|
+
<path strokeWidth="3" d="M0 0v-26" /><circle cy="-26" r="4.5" opacity="0.85" />
|
|
16
|
+
<path strokeWidth="3" d="m0 0 13-22" /><circle cx="13" cy="-22" r="3.5" opacity="0.78" />
|
|
17
|
+
<path strokeWidth="3" d="m0 0 23-9" /><circle cx="23" cy="-9" r="4" opacity="0.72" />
|
|
18
|
+
<path strokeWidth="2.5" opacity="0.7" d="m0 0 21 11" /><circle cx="21" cy="11" r="3" opacity="0.6" />
|
|
19
|
+
<path strokeWidth="2.5" opacity="0.55" d="m0 0 9 23" /><circle cx="9" cy="23" r="3.5" opacity="0.5" />
|
|
20
|
+
<path strokeWidth="2.5" opacity="0.55" d="m0 0-11 22" /><circle cx="-11" cy="22" r="3" opacity="0.48" />
|
|
21
|
+
<path strokeWidth="3" d="m0 0-26 3" /><circle cx="-26" cy="3" r="4" opacity="0.75" />
|
|
22
|
+
<path strokeWidth="3" d="m0 0-20-17" /><circle cx="-20" cy="-17" r="4.5" opacity="0.85" />
|
|
23
|
+
<path strokeWidth="2" opacity="0.66" d="m0 0-8-23" /><circle cx="-8" cy="-23" r="3" opacity="0.62" />
|
|
24
|
+
</g>
|
|
25
|
+
</svg>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function Icon({ name, size }) {
|
|
30
|
+
const style = size ? { fontSize: size } : undefined;
|
|
31
|
+
return <i className={`fa-solid fa-${name}`} style={style} aria-hidden="true" />;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function StateTag({ state }) {
|
|
35
|
+
return <span className={`tag state-${state}`}>{state}</span>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function StabilityTag({ stability }) {
|
|
39
|
+
return <span className={`tag stability-${stability}`}>{stability}</span>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function Chip({ children, title }) {
|
|
43
|
+
return <span className="chip" title={title}>{children}</span>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function TemplateBadge({ name, colour }) {
|
|
47
|
+
const style = colour ? { background: colour } : { background: 'var(--ink-4)' };
|
|
48
|
+
return <span className="template-badge" style={style}>{name}</span>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function PropertyValue({ value, onNavigate }) {
|
|
52
|
+
if (value === null || value === undefined) return <span className="prop-empty">-</span>;
|
|
53
|
+
|
|
54
|
+
if (typeof value === 'object' && 'display' in value && 'nodeId' in value) {
|
|
55
|
+
return (
|
|
56
|
+
<a className="node-ref-link" onClick={() => onNavigate && onNavigate(value.nodeId)}>
|
|
57
|
+
{value.display}
|
|
58
|
+
</a>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (typeof value === 'object' && 'display' in value) {
|
|
63
|
+
return <span>{value.display}</span>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (Array.isArray(value)) {
|
|
67
|
+
return <span>{value.length === 1 ? '1 item' : `${value.length} items`}</span>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (typeof value === 'object') {
|
|
71
|
+
return <span className="prop-empty">-</span>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return <span>{String(value)}</span>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildPropertyRows(entries, onNavigate, depth = 0, parentPath = '') {
|
|
78
|
+
return entries.flatMap(([key, value], index) => {
|
|
79
|
+
const rowPath = parentPath ? `${parentPath}.${key}` : key;
|
|
80
|
+
const rowKey = `${rowPath}:${index}`;
|
|
81
|
+
|
|
82
|
+
if (Array.isArray(value)) {
|
|
83
|
+
const rows = [{
|
|
84
|
+
key: rowKey,
|
|
85
|
+
label: key,
|
|
86
|
+
depth,
|
|
87
|
+
value: <PropertyValue value={value} onNavigate={onNavigate} />,
|
|
88
|
+
}];
|
|
89
|
+
|
|
90
|
+
value.forEach((item, itemIndex) => {
|
|
91
|
+
const itemPath = `${rowPath}[${itemIndex}]`;
|
|
92
|
+
if (item && typeof item === 'object' && !('display' in item) && !Array.isArray(item)) {
|
|
93
|
+
rows.push({
|
|
94
|
+
key: `${itemPath}:group`,
|
|
95
|
+
label: `[${itemIndex}]`,
|
|
96
|
+
depth: depth + 1,
|
|
97
|
+
value: <span className="prop-empty">-</span>,
|
|
98
|
+
});
|
|
99
|
+
rows.push(...buildPropertyRows(Object.entries(item), onNavigate, depth + 2, itemPath));
|
|
100
|
+
} else {
|
|
101
|
+
rows.push({
|
|
102
|
+
key: `${itemPath}:value`,
|
|
103
|
+
label: `[${itemIndex}]`,
|
|
104
|
+
depth: depth + 1,
|
|
105
|
+
value: <PropertyValue value={item} onNavigate={onNavigate} />,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return rows;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (value && typeof value === 'object' && !('display' in value)) {
|
|
114
|
+
return [
|
|
115
|
+
{
|
|
116
|
+
key: rowKey,
|
|
117
|
+
label: key,
|
|
118
|
+
depth,
|
|
119
|
+
value: <span className="prop-empty">-</span>,
|
|
120
|
+
},
|
|
121
|
+
...buildPropertyRows(Object.entries(value), onNavigate, depth + 1, rowPath),
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return [{
|
|
126
|
+
key: rowKey,
|
|
127
|
+
label: key,
|
|
128
|
+
depth,
|
|
129
|
+
value: <PropertyValue value={value} onNavigate={onNavigate} />,
|
|
130
|
+
}];
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function PropertiesTable({ properties, onNavigate }) {
|
|
135
|
+
const entries = Object.entries(properties ?? {});
|
|
136
|
+
const rows = buildPropertyRows(entries, onNavigate);
|
|
137
|
+
if (entries.length === 0) {
|
|
138
|
+
return <p className="label-sm" style={{ padding: '10px 14px' }}>No properties.</p>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<table className="prop-table">
|
|
143
|
+
<tbody>
|
|
144
|
+
{rows.map(row => (
|
|
145
|
+
<tr key={row.key} className={`prop-row${row.depth > 0 ? ' nested' : ''}`}>
|
|
146
|
+
<td className="mono prop-key-cell">
|
|
147
|
+
<span className="prop-key-label" style={{ '--prop-depth': row.depth }}>
|
|
148
|
+
{row.label}
|
|
149
|
+
</span>
|
|
150
|
+
</td>
|
|
151
|
+
<td>{row.value}</td>
|
|
152
|
+
</tr>
|
|
153
|
+
))}
|
|
154
|
+
</tbody>
|
|
155
|
+
</table>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function localSchemaName(nodeId) {
|
|
160
|
+
const marker = '.schemas.';
|
|
161
|
+
const idx = nodeId.indexOf(marker);
|
|
162
|
+
if (idx < 0) return nodeId.split('.').pop();
|
|
163
|
+
return nodeId.slice(idx + marker.length).split('.')[0];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function fieldSchemaName(nodeId) {
|
|
167
|
+
const schemaMarker = '.schemas.';
|
|
168
|
+
const fieldMarker = '.fields.';
|
|
169
|
+
const schemaIdx = nodeId.indexOf(schemaMarker);
|
|
170
|
+
const fieldIdx = nodeId.indexOf(fieldMarker);
|
|
171
|
+
if (schemaIdx < 0 || fieldIdx < 0) return null;
|
|
172
|
+
return nodeId.slice(schemaIdx + schemaMarker.length, fieldIdx);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function fieldLocalName(nodeId) {
|
|
176
|
+
const marker = '.fields.';
|
|
177
|
+
const idx = nodeId.indexOf(marker);
|
|
178
|
+
return idx < 0 ? nodeId.split('.').pop() : nodeId.slice(idx + marker.length);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function clusterNodeId(nodeId) {
|
|
182
|
+
const sectionMatch = nodeId.match(/\.(schemas|enums|operations)\./);
|
|
183
|
+
if (sectionMatch && sectionMatch.index !== undefined) {
|
|
184
|
+
return nodeId.slice(0, sectionMatch.index);
|
|
185
|
+
}
|
|
186
|
+
return nodeId.replace(/\.(fields|values)\.[^.]+$/, '');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function refName(ref) {
|
|
190
|
+
if (typeof ref === 'string') return ref.replace(/^#\/(schemas|enums)\//, '');
|
|
191
|
+
if (ref && typeof ref === 'object' && 'display' in ref) return ref.display;
|
|
192
|
+
return String(ref);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function refLocalSchemaName(ref) {
|
|
196
|
+
if (typeof ref === 'string') return ref.startsWith('#/schemas/') ? ref.slice(10) : null;
|
|
197
|
+
if (ref && typeof ref === 'object' && 'display' in ref) return ref.display;
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function fieldType(properties) {
|
|
202
|
+
const cardinality = properties?.cardinality === 'many' ? '[]' : '';
|
|
203
|
+
if (properties?.type) return `${properties.type}${cardinality}`;
|
|
204
|
+
const ref = properties?.['$ref'];
|
|
205
|
+
if (ref) return `${refName(ref)}${cardinality}`;
|
|
206
|
+
return cardinality ? `unknown${cardinality}` : 'unknown';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function fieldRequirement(properties) {
|
|
210
|
+
if (properties?.nullable === false) return 'required';
|
|
211
|
+
if (properties?.nullable === true) return 'optional';
|
|
212
|
+
return '-';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function fieldCardinality(properties) {
|
|
216
|
+
return properties?.cardinality ?? '-';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function fieldDetails(properties) {
|
|
220
|
+
const parts = [];
|
|
221
|
+
const ref = properties?.['$ref'];
|
|
222
|
+
if (ref) parts.push(`ref ${refName(ref)}`);
|
|
223
|
+
if (properties?.description) parts.push(properties.description);
|
|
224
|
+
return parts.length > 0 ? parts.join(' · ') : '-';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function linkSummary(edge, fieldId) {
|
|
228
|
+
const outgoing = edge.from === fieldId;
|
|
229
|
+
const otherNodeId = outgoing ? edge.to : edge.from;
|
|
230
|
+
const direction = outgoing ? '->' : '<-';
|
|
231
|
+
const relation = edge.type === 'maps-to' ? '' : `${edge.type} `;
|
|
232
|
+
return {
|
|
233
|
+
direction,
|
|
234
|
+
label: `${relation}${fieldLocalName(otherNodeId)}`,
|
|
235
|
+
targetNodeId: clusterNodeId(otherNodeId),
|
|
236
|
+
title: otherNodeId,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function localEnumName(nodeId) {
|
|
241
|
+
const marker = '.enums.';
|
|
242
|
+
const idx = nodeId.indexOf(marker);
|
|
243
|
+
if (idx < 0) return nodeId.split('.').pop();
|
|
244
|
+
return nodeId.slice(idx + marker.length).split('.')[0];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function enumValueEnumName(nodeId) {
|
|
248
|
+
const enumMarker = '.enums.';
|
|
249
|
+
const valueMarker = '.values.';
|
|
250
|
+
const enumIdx = nodeId.indexOf(enumMarker);
|
|
251
|
+
const valueIdx = nodeId.indexOf(valueMarker);
|
|
252
|
+
if (enumIdx < 0 || valueIdx < 0) return null;
|
|
253
|
+
return nodeId.slice(enumIdx + enumMarker.length, valueIdx);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function enumValueDisplayName(node) {
|
|
257
|
+
return node.properties?.name ?? node.id.split('.').pop();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function enumValueDescription(node) {
|
|
261
|
+
return node.properties?.description ?? '-';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function buildSchemaModel(schemaNodes, allNodes) {
|
|
265
|
+
const schemasByName = new Map(schemaNodes.map(node => [localSchemaName(node.id), node]));
|
|
266
|
+
const fieldsBySchema = new Map();
|
|
267
|
+
const referencedSchemas = new Set();
|
|
268
|
+
|
|
269
|
+
for (const node of allNodes ?? []) {
|
|
270
|
+
if (node.template !== 'Field') continue;
|
|
271
|
+
const schemaName = fieldSchemaName(node.id);
|
|
272
|
+
if (!schemaName) continue;
|
|
273
|
+
if (!fieldsBySchema.has(schemaName)) fieldsBySchema.set(schemaName, []);
|
|
274
|
+
fieldsBySchema.get(schemaName).push(node);
|
|
275
|
+
|
|
276
|
+
const ref = node.properties?.['$ref'];
|
|
277
|
+
const localName = refLocalSchemaName(ref);
|
|
278
|
+
if (localName && schemasByName.has(localName)) {
|
|
279
|
+
referencedSchemas.add(localName);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const topSchemas = schemaNodes.filter(node => !referencedSchemas.has(localSchemaName(node.id)));
|
|
284
|
+
return {
|
|
285
|
+
schemasByName,
|
|
286
|
+
fieldsBySchema,
|
|
287
|
+
topSchemas: topSchemas.length > 0 ? topSchemas : schemaNodes,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function SchemaFieldRows({ schemaName, model, prefix = '', depth = 0, visited = new Set(), edges = [], overlayFields, overlayRefs }) {
|
|
292
|
+
const fields = model.fieldsBySchema.get(schemaName) ?? [];
|
|
293
|
+
if (fields.length === 0) {
|
|
294
|
+
return (
|
|
295
|
+
<div className="field-row">
|
|
296
|
+
<div />
|
|
297
|
+
<div className="label-sm">No fields.</div>
|
|
298
|
+
<div />
|
|
299
|
+
<div />
|
|
300
|
+
<div />
|
|
301
|
+
<div />
|
|
302
|
+
<div />
|
|
303
|
+
</div>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<>
|
|
309
|
+
{fields.map(field => {
|
|
310
|
+
const name = fieldLocalName(field.id);
|
|
311
|
+
const ref = field.properties?.['$ref'];
|
|
312
|
+
const localRef = refLocalSchemaName(ref);
|
|
313
|
+
const canExpand = localRef !== null && model.schemasByName.has(localRef) && !visited.has(localRef);
|
|
314
|
+
const childSchemaNode = canExpand ? model.schemasByName.get(localRef) : null;
|
|
315
|
+
const childGhostFields = childSchemaNode ? overlayFieldsForSchema(overlayFields, childSchemaNode.id) : [];
|
|
316
|
+
const childPrefix = `${prefix}${name}${field.properties?.cardinality === 'many' ? '[].' : '.'}`;
|
|
317
|
+
const nextVisited = new Set(visited);
|
|
318
|
+
nextVisited.add(schemaName);
|
|
319
|
+
const links = edges.filter(e =>
|
|
320
|
+
(e.from === field.id || e.to === field.id)
|
|
321
|
+
&& e.type !== 'has-field'
|
|
322
|
+
&& e.type !== 'has-value',
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<React.Fragment key={field.id}>
|
|
327
|
+
<div className={`field-row${depth > 0 ? ' nested' : ''}`} style={{ '--field-depth': depth }}>
|
|
328
|
+
<div className="gutter">{canExpand && <Icon name="caret-down" size={11} />}</div>
|
|
329
|
+
<div className="name">{prefix}{name}</div>
|
|
330
|
+
<div className="type">{fieldType(field.properties)}</div>
|
|
331
|
+
<div className="cardinality">{fieldCardinality(field.properties)}</div>
|
|
332
|
+
<div className="req">{fieldRequirement(field.properties)}</div>
|
|
333
|
+
<div className="state"><StateTag state={field.state} /></div>
|
|
334
|
+
<div className="lineage">
|
|
335
|
+
{links.length > 0
|
|
336
|
+
? links.map((edge, index) => {
|
|
337
|
+
const link = linkSummary(edge, field.id);
|
|
338
|
+
return (
|
|
339
|
+
<React.Fragment key={edge.id}>
|
|
340
|
+
{index > 0 && <span key={`sep-${edge.id}`}>{' '}</span>}
|
|
341
|
+
<a
|
|
342
|
+
className="node-ref-link"
|
|
343
|
+
onClick={() => navigate(`/node?id=${encodeURIComponent(link.targetNodeId)}`)}
|
|
344
|
+
title={link.title}
|
|
345
|
+
>
|
|
346
|
+
{link.direction} {link.label}
|
|
347
|
+
</a>
|
|
348
|
+
</React.Fragment>
|
|
349
|
+
);
|
|
350
|
+
})
|
|
351
|
+
: fieldDetails(field.properties)}
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
{canExpand && (
|
|
355
|
+
<>
|
|
356
|
+
<SchemaFieldRows
|
|
357
|
+
schemaName={localRef}
|
|
358
|
+
model={model}
|
|
359
|
+
prefix={childPrefix}
|
|
360
|
+
depth={depth + 1}
|
|
361
|
+
visited={nextVisited}
|
|
362
|
+
edges={edges}
|
|
363
|
+
overlayFields={overlayFields}
|
|
364
|
+
overlayRefs={overlayRefs}
|
|
365
|
+
/>
|
|
366
|
+
{childGhostFields.length > 0 && (
|
|
367
|
+
<GhostFieldRows fields={childGhostFields} overlayRefs={overlayRefs} prefix={childPrefix} depth={depth + 1} />
|
|
368
|
+
)}
|
|
369
|
+
</>
|
|
370
|
+
)}
|
|
371
|
+
</React.Fragment>
|
|
372
|
+
);
|
|
373
|
+
})}
|
|
374
|
+
</>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Max 2 distinct stripe colours — OverlayLegend also only renders 2 entries.
|
|
379
|
+
function ghostStripeClass(index) {
|
|
380
|
+
if (index === 0) return 'overlay-stripe-0';
|
|
381
|
+
if (index === 1) return 'overlay-stripe-1';
|
|
382
|
+
return 'overlay-stripe-0';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function overlayFieldsForSchema(overlayFields, schemaNodeId) {
|
|
386
|
+
if (!overlayFields) return [];
|
|
387
|
+
const prefix = schemaNodeId + '.fields.';
|
|
388
|
+
return overlayFields.filter(field => field.id.startsWith(prefix));
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function GhostFieldRows({ fields, overlayRefs, prefix = '', depth = 0 }) {
|
|
392
|
+
return (
|
|
393
|
+
<>
|
|
394
|
+
{fields.map(field => {
|
|
395
|
+
const isConflict = field.ghostState === 'ghost-conflict' || field.ghostState === 'local-modified';
|
|
396
|
+
const refIndex = overlayRefs ? overlayRefs.indexOf(field.sourceRef) : 0;
|
|
397
|
+
const stripeClass = isConflict ? 'overlay-conflict' : ghostStripeClass(Math.max(0, refIndex));
|
|
398
|
+
const name = prefix + (fieldLocalName(field.id));
|
|
399
|
+
const type = field.node.properties?.type || (field.node.properties?.['$ref'] ? String(field.node.properties['$ref']).replace(/^#\/(schemas|enums)\//, '') : '-');
|
|
400
|
+
return (
|
|
401
|
+
<div
|
|
402
|
+
key={field.id}
|
|
403
|
+
className={`field-row overlay-ghost ${stripeClass}${depth > 0 ? ' nested' : ''}`}
|
|
404
|
+
style={{ '--field-depth': depth }}
|
|
405
|
+
title={`From ${field.sourceRef}`}
|
|
406
|
+
>
|
|
407
|
+
<div className="gutter">
|
|
408
|
+
{isConflict && <span style={{ color: '#c44', fontWeight: 700 }}>!</span>}
|
|
409
|
+
</div>
|
|
410
|
+
<div className="name">{name}</div>
|
|
411
|
+
<div className="type">{type}</div>
|
|
412
|
+
<div className="cardinality">-</div>
|
|
413
|
+
<div className="req">-</div>
|
|
414
|
+
<div className="state">
|
|
415
|
+
{isConflict
|
|
416
|
+
? <span className="tag" style={{ background: '#c4422222', color: '#c44' }}>conflict</span>
|
|
417
|
+
: <StateTag state={field.node.state} />
|
|
418
|
+
}
|
|
419
|
+
</div>
|
|
420
|
+
<div className="lineage">
|
|
421
|
+
<span className="mono" style={{ fontSize: 10.5, opacity: 0.7 }}>{field.sourceRef}</span>
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
);
|
|
425
|
+
})}
|
|
426
|
+
</>
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function OverlayLegend({ overlayRefs }) {
|
|
431
|
+
if (!overlayRefs || overlayRefs.length === 0) return null;
|
|
432
|
+
const colours = ['var(--accent)', '#5c7aa8'];
|
|
433
|
+
return (
|
|
434
|
+
<div className="overlay-legend">
|
|
435
|
+
<span className="label-xs">Incoming:</span>
|
|
436
|
+
{overlayRefs.map((ref, index) => (
|
|
437
|
+
<span key={ref} className="overlay-legend-item">
|
|
438
|
+
<span
|
|
439
|
+
className="overlay-legend-swatch"
|
|
440
|
+
style={{ background: colours[index % colours.length] }}
|
|
441
|
+
/>
|
|
442
|
+
<span className="mono" style={{ fontSize: 10.5 }}>{ref}</span>
|
|
443
|
+
</span>
|
|
444
|
+
))}
|
|
445
|
+
<span className="overlay-legend-item" style={{ color: '#c44' }}>
|
|
446
|
+
<span className="overlay-legend-swatch" style={{ background: '#c44' }} />
|
|
447
|
+
conflict
|
|
448
|
+
</span>
|
|
449
|
+
</div>
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function SchemaCard({ title, nodes, allNodes, edges, anchorIdForNode, overlayFields, overlayRefs }) {
|
|
454
|
+
if (!nodes || nodes.length === 0) return null;
|
|
455
|
+
|
|
456
|
+
if (title === 'EnumDefinition') {
|
|
457
|
+
const valuesByEnum = new Map();
|
|
458
|
+
for (const node of allNodes ?? []) {
|
|
459
|
+
if (node.template !== 'EnumValue') continue;
|
|
460
|
+
const enumName = enumValueEnumName(node.id);
|
|
461
|
+
if (!enumName) continue;
|
|
462
|
+
if (!valuesByEnum.has(enumName)) valuesByEnum.set(enumName, []);
|
|
463
|
+
valuesByEnum.get(enumName).push(node);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return (
|
|
467
|
+
<div className="card enum-card">
|
|
468
|
+
<div className="card-head">Enums</div>
|
|
469
|
+
<div className="card-body">
|
|
470
|
+
{nodes.map(enumNode => {
|
|
471
|
+
const enumName = localEnumName(enumNode.id);
|
|
472
|
+
const values = valuesByEnum.get(enumName) ?? [];
|
|
473
|
+
return (
|
|
474
|
+
<div key={enumNode.id} className="enum-section" id={anchorIdForNode ? anchorIdForNode(enumNode.id) : undefined}>
|
|
475
|
+
<div className="schema-section-head">
|
|
476
|
+
<div>
|
|
477
|
+
<div className="schema-title">{enumName}</div>
|
|
478
|
+
{enumNode.properties?.description && <div className="label-sm">{enumNode.properties.description}</div>}
|
|
479
|
+
</div>
|
|
480
|
+
<div className="label-sm mono">{enumNode.id}</div>
|
|
481
|
+
</div>
|
|
482
|
+
<div className="enum-row enum-row-head">
|
|
483
|
+
<div className="label-xs">Name</div>
|
|
484
|
+
<div className="label-xs">Description</div>
|
|
485
|
+
<div className="label-xs">Status</div>
|
|
486
|
+
</div>
|
|
487
|
+
{values.length === 0 ? (
|
|
488
|
+
<div className="enum-row">
|
|
489
|
+
<div className="label-sm">No values.</div>
|
|
490
|
+
<div />
|
|
491
|
+
<div />
|
|
492
|
+
</div>
|
|
493
|
+
) : values.map(value => (
|
|
494
|
+
<div key={value.id} className="enum-row">
|
|
495
|
+
<div className="name">{enumValueDisplayName(value)}</div>
|
|
496
|
+
<div className="description">{enumValueDescription(value)}</div>
|
|
497
|
+
<div className="state"><StateTag state={value.state} /></div>
|
|
498
|
+
</div>
|
|
499
|
+
))}
|
|
500
|
+
</div>
|
|
501
|
+
);
|
|
502
|
+
})}
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (title === 'Schema') {
|
|
509
|
+
const model = buildSchemaModel(nodes, allNodes ?? nodes);
|
|
510
|
+
return (
|
|
511
|
+
<div className="card schema-card">
|
|
512
|
+
<div className="card-head">Schemas</div>
|
|
513
|
+
<div className="card-body">
|
|
514
|
+
{model.topSchemas.map(schema => {
|
|
515
|
+
const schemaName = localSchemaName(schema.id);
|
|
516
|
+
const ghostFields = overlayFieldsForSchema(overlayFields, schema.id);
|
|
517
|
+
return (
|
|
518
|
+
<div key={schema.id} className="schema-section" id={anchorIdForNode ? anchorIdForNode(schema.id) : undefined}>
|
|
519
|
+
<div className="schema-section-head">
|
|
520
|
+
<div>
|
|
521
|
+
<div className="schema-title">{schemaName}</div>
|
|
522
|
+
{schema.properties?.description && <div className="label-sm">{schema.properties.description}</div>}
|
|
523
|
+
</div>
|
|
524
|
+
<div className="label-sm mono">{schema.id}</div>
|
|
525
|
+
</div>
|
|
526
|
+
{ghostFields.length > 0 && <OverlayLegend overlayRefs={overlayRefs} />}
|
|
527
|
+
<div className="field-row field-row-head">
|
|
528
|
+
<div />
|
|
529
|
+
<div className="label-xs">Name</div>
|
|
530
|
+
<div className="label-xs">Type</div>
|
|
531
|
+
<div className="label-xs">Cardinality</div>
|
|
532
|
+
<div className="label-xs">Req</div>
|
|
533
|
+
<div className="label-xs">State</div>
|
|
534
|
+
<div className="label-xs">Links</div>
|
|
535
|
+
</div>
|
|
536
|
+
<SchemaFieldRows
|
|
537
|
+
schemaName={schemaName}
|
|
538
|
+
model={model}
|
|
539
|
+
visited={new Set()}
|
|
540
|
+
edges={edges ?? []}
|
|
541
|
+
overlayFields={overlayFields}
|
|
542
|
+
overlayRefs={overlayRefs}
|
|
543
|
+
/>
|
|
544
|
+
{ghostFields.length > 0 && (
|
|
545
|
+
<GhostFieldRows fields={ghostFields} overlayRefs={overlayRefs} />
|
|
546
|
+
)}
|
|
547
|
+
</div>
|
|
548
|
+
);
|
|
549
|
+
})}
|
|
550
|
+
</div>
|
|
551
|
+
</div>
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return (
|
|
556
|
+
<div className="card">
|
|
557
|
+
<div className="card-head">{title}</div>
|
|
558
|
+
<div className="card-body">
|
|
559
|
+
{nodes.map(node => (
|
|
560
|
+
<div key={node.id} id={anchorIdForNode ? anchorIdForNode(node.id) : undefined} style={{ borderBottom: '1px dashed var(--rule)' }}>
|
|
561
|
+
<div className="mono" style={{ padding: '8px 14px 0', color: 'var(--ink-3)', fontSize: 11, fontWeight: 600 }}>
|
|
562
|
+
{node.id.split('.').pop()}
|
|
563
|
+
</div>
|
|
564
|
+
<PropertiesTable properties={node.properties} />
|
|
565
|
+
</div>
|
|
566
|
+
))}
|
|
567
|
+
</div>
|
|
568
|
+
</div>
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
window.CorumPrimitives = {
|
|
573
|
+
navigate,
|
|
574
|
+
BrandMark,
|
|
575
|
+
Icon,
|
|
576
|
+
StateTag,
|
|
577
|
+
StabilityTag,
|
|
578
|
+
Chip,
|
|
579
|
+
TemplateBadge,
|
|
580
|
+
PropertyValue,
|
|
581
|
+
PropertiesTable,
|
|
582
|
+
SchemaCard,
|
|
583
|
+
};
|