@finos/legend-application-studio 28.20.0 → 28.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/lib/__lib__/LegendStudioUserDataHelper.d.ts +4 -1
  2. package/lib/__lib__/LegendStudioUserDataHelper.d.ts.map +1 -1
  3. package/lib/__lib__/LegendStudioUserDataHelper.js +18 -0
  4. package/lib/__lib__/LegendStudioUserDataHelper.js.map +1 -1
  5. package/lib/components/editor/editor-group/EditorGroup.d.ts.map +1 -1
  6. package/lib/components/editor/editor-group/EditorGroup.js +5 -0
  7. package/lib/components/editor/editor-group/EditorGroup.js.map +1 -1
  8. package/lib/components/editor/editor-group/dataProduct/DataProductEditor.d.ts.map +1 -1
  9. package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js +13 -44
  10. package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js.map +1 -1
  11. package/lib/components/editor/editor-group/database-editor/DatabaseAnnotationDisplay.d.ts +26 -0
  12. package/lib/components/editor/editor-group/database-editor/DatabaseAnnotationDisplay.d.ts.map +1 -0
  13. package/lib/components/editor/editor-group/database-editor/DatabaseAnnotationDisplay.js +101 -0
  14. package/lib/components/editor/editor-group/database-editor/DatabaseAnnotationDisplay.js.map +1 -0
  15. package/lib/components/editor/editor-group/database-editor/DatabaseDiagramCanvas.d.ts +23 -0
  16. package/lib/components/editor/editor-group/database-editor/DatabaseDiagramCanvas.d.ts.map +1 -0
  17. package/lib/components/editor/editor-group/database-editor/DatabaseDiagramCanvas.js +434 -0
  18. package/lib/components/editor/editor-group/database-editor/DatabaseDiagramCanvas.js.map +1 -0
  19. package/lib/components/editor/editor-group/database-editor/DatabaseDiagramHelper.d.ts +242 -0
  20. package/lib/components/editor/editor-group/database-editor/DatabaseDiagramHelper.d.ts.map +1 -0
  21. package/lib/components/editor/editor-group/database-editor/DatabaseDiagramHelper.js +371 -0
  22. package/lib/components/editor/editor-group/database-editor/DatabaseDiagramHelper.js.map +1 -0
  23. package/lib/components/editor/editor-group/database-editor/DatabaseEditor.d.ts +29 -0
  24. package/lib/components/editor/editor-group/database-editor/DatabaseEditor.d.ts.map +1 -0
  25. package/lib/components/editor/editor-group/database-editor/DatabaseEditor.js +78 -0
  26. package/lib/components/editor/editor-group/database-editor/DatabaseEditor.js.map +1 -0
  27. package/lib/components/editor/editor-group/database-editor/DatabaseSchemaTree.d.ts +30 -0
  28. package/lib/components/editor/editor-group/database-editor/DatabaseSchemaTree.d.ts.map +1 -0
  29. package/lib/components/editor/editor-group/database-editor/DatabaseSchemaTree.js +331 -0
  30. package/lib/components/editor/editor-group/database-editor/DatabaseSchemaTree.js.map +1 -0
  31. package/lib/components/editor/editor-group/database-editor/DatabaseTableNode.d.ts +104 -0
  32. package/lib/components/editor/editor-group/database-editor/DatabaseTableNode.d.ts.map +1 -0
  33. package/lib/components/editor/editor-group/database-editor/DatabaseTableNode.js +151 -0
  34. package/lib/components/editor/editor-group/database-editor/DatabaseTableNode.js.map +1 -0
  35. package/lib/components/editor/editor-group/ingest-editor/IngestDefinitionEditor.d.ts.map +1 -1
  36. package/lib/components/editor/editor-group/ingest-editor/IngestDefinitionEditor.js +3 -78
  37. package/lib/components/editor/editor-group/ingest-editor/IngestDefinitionEditor.js.map +1 -1
  38. package/lib/index.css +2 -2
  39. package/lib/index.css.map +1 -1
  40. package/lib/package.json +4 -1
  41. package/lib/stores/editor/EditorTabManagerState.d.ts.map +1 -1
  42. package/lib/stores/editor/EditorTabManagerState.js +5 -3
  43. package/lib/stores/editor/EditorTabManagerState.js.map +1 -1
  44. package/lib/stores/editor/editor-state/element-editor-state/DatabaseEditorState.d.ts +252 -0
  45. package/lib/stores/editor/editor-state/element-editor-state/DatabaseEditorState.d.ts.map +1 -0
  46. package/lib/stores/editor/editor-state/element-editor-state/DatabaseEditorState.js +755 -0
  47. package/lib/stores/editor/editor-state/element-editor-state/DatabaseEditorState.js.map +1 -0
  48. package/package.json +12 -9
  49. package/src/__lib__/LegendStudioUserDataHelper.ts +30 -0
  50. package/src/components/editor/editor-group/EditorGroup.tsx +4 -0
  51. package/src/components/editor/editor-group/dataProduct/DataProductEditor.tsx +0 -52
  52. package/src/components/editor/editor-group/database-editor/DatabaseAnnotationDisplay.tsx +200 -0
  53. package/src/components/editor/editor-group/database-editor/DatabaseDiagramCanvas.tsx +701 -0
  54. package/src/components/editor/editor-group/database-editor/DatabaseDiagramHelper.ts +555 -0
  55. package/src/components/editor/editor-group/database-editor/DatabaseEditor.tsx +246 -0
  56. package/src/components/editor/editor-group/database-editor/DatabaseSchemaTree.tsx +1053 -0
  57. package/src/components/editor/editor-group/database-editor/DatabaseTableNode.tsx +465 -0
  58. package/src/components/editor/editor-group/ingest-editor/IngestDefinitionEditor.tsx +2 -242
  59. package/src/stores/editor/EditorTabManagerState.ts +4 -5
  60. package/src/stores/editor/editor-state/element-editor-state/DatabaseEditorState.ts +938 -0
  61. package/tsconfig.json +7 -0
@@ -0,0 +1,465 @@
1
+ /**
2
+ * Copyright (c) 2020-present, Goldman Sachs
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { Handle, Position, type NodeProps } from '@xyflow/react';
18
+ import {
19
+ EyeIcon,
20
+ KeyIcon,
21
+ PURE_DatabaseIcon,
22
+ PURE_DatabaseTableIcon,
23
+ clsx,
24
+ } from '@finos/legend-art';
25
+ import { observer } from 'mobx-react-lite';
26
+ import type { ReactElement } from 'react';
27
+ import type { Column, Table, View } from '@finos/legend-graph';
28
+ import {
29
+ getColumnTypeLabel,
30
+ getTableColumns,
31
+ isPrimaryKey,
32
+ resolveViewColumnFormula,
33
+ resolveViewGroupByFormula,
34
+ summarizeMilestoning,
35
+ } from './DatabaseDiagramHelper.js';
36
+ import { DatabaseAnnotationDisplay } from './DatabaseAnnotationDisplay.js';
37
+
38
+ /**
39
+ * Discriminator for the two relation kinds the canvas renders. Drives icon
40
+ * choice, column-row content (type vs. formula), and the SCSS color tint.
41
+ */
42
+ export type DatabaseTableNodeKind = 'table' | 'view';
43
+
44
+ export interface DatabaseTableNodeData extends Record<string, unknown> {
45
+ /** The underlying Table or View — narrow with `kind` before accessing
46
+ * kind-specific fields like `Table.primaryKey` or `View.columnMappings`. */
47
+ relation: Table | View;
48
+ kind: DatabaseTableNodeKind;
49
+ schemaName: string;
50
+ /** True when THIS relation is the currently selected one (blue ring). */
51
+ isSelected: boolean;
52
+ /** True when THIS relation is one of the two endpoints of the currently
53
+ * selected join (yellow ring — visually distinct from blue selection). */
54
+ isJoinEndpoint: boolean;
55
+ /** Columns that participate in any join in the database. Used to tint
56
+ * table columns in blue ("FK-like"). Doesn't apply to views. */
57
+ fkColumns: Set<Column>;
58
+ /** Column currently focused via the side-panel. Drives the single-row
59
+ * highlight inside the matching table node. Tables only — views use
60
+ * `selectedViewColumnName` since their "columns" are mapping names. */
61
+ selectedColumn: Column | undefined;
62
+ /** Name of the view column-mapping currently focused via the side panel.
63
+ * Mirrors `selectedColumn` but for views, where mappings don't have
64
+ * `Column` instances. Always `undefined` for table-kind nodes. */
65
+ selectedViewColumnName: string | undefined;
66
+ /**
67
+ * Lookup table for view-column Pure-code formulas, keyed by
68
+ * `<schema>.<view>.<column>`. Forwarded from the editor state via the
69
+ * canvas. Empty until `loadViewColumnFormulas` resolves; consumers fall
70
+ * back to a static placeholder per `resolveViewColumnFormula`. Empty for
71
+ * table-kind nodes (we still pass it for prop-shape stability).
72
+ */
73
+ viewColumnFormulas: ReadonlyMap<string, string>;
74
+ /**
75
+ * Lookup table for view groupBy Pure-code expressions, keyed by
76
+ * `<schema>.<view>.groupBy[<index>]`. Forwarded from the editor state
77
+ * via the canvas alongside `viewColumnFormulas`; same lazy-load story
78
+ * with a separate static placeholder per `resolveViewGroupByFormula`.
79
+ * Empty for table-kind nodes and for views with no groupBy.
80
+ */
81
+ viewGroupByFormulas: ReadonlyMap<string, string>;
82
+ }
83
+
84
+ /**
85
+ * View-only React Flow node representing a single relation (Table or View).
86
+ *
87
+ * Layout: header (icon + relation name + schema badge + optional VIEW tag) +
88
+ * a list of column rows. Each row is a fixed-height grid:
89
+ * - Tables: [PK key icon | column name | column type]
90
+ * - Views: [bullet | column name | formula placeholder]
91
+ *
92
+ * Two invisible Handles (left/right) let React Flow route edges into either
93
+ * side of the box without committing to a specific column anchor.
94
+ */
95
+ export const DatabaseTableNode = observer(
96
+ (props: NodeProps & { data: DatabaseTableNodeData }) => {
97
+ const {
98
+ relation,
99
+ kind,
100
+ schemaName,
101
+ isSelected,
102
+ isJoinEndpoint,
103
+ fkColumns,
104
+ selectedColumn,
105
+ selectedViewColumnName,
106
+ viewColumnFormulas,
107
+ viewGroupByFormulas,
108
+ } = props.data;
109
+ const isViewKind = kind === 'view';
110
+ return (
111
+ <div
112
+ className={clsx('database-diagram__table-node', {
113
+ 'database-diagram__table-node--selected': isSelected,
114
+ 'database-diagram__table-node--join-endpoint': isJoinEndpoint,
115
+ 'database-diagram__table-node--view': isViewKind,
116
+ })}
117
+ >
118
+ <Handle
119
+ type="target"
120
+ position={Position.Left}
121
+ className="database-diagram__table-node__handle"
122
+ isConnectable={false}
123
+ />
124
+ <Handle
125
+ type="source"
126
+ position={Position.Right}
127
+ className="database-diagram__table-node__handle"
128
+ isConnectable={false}
129
+ />
130
+
131
+ <div className="database-diagram__table-node__header">
132
+ <div className="database-diagram__table-node__header__icon">
133
+ {isViewKind ? <EyeIcon /> : <PURE_DatabaseTableIcon />}
134
+ </div>
135
+ <div className="database-diagram__table-node__header__name">
136
+ {relation.name}
137
+ </div>
138
+ {isViewKind && (
139
+ <div className="database-diagram__table-node__header__kind-tag">
140
+ VIEW
141
+ </div>
142
+ )}
143
+ {isViewKind && renderViewMetadataTags(relation as View)}
144
+ {!isViewKind && renderMilestoningTags(relation as Table)}
145
+ <div className="database-diagram__table-node__header__schema">
146
+ {schemaName}
147
+ </div>
148
+ {/*
149
+ * Compact annotation badge — shows only the count plus a tag
150
+ * icon, with the full content surfaced via tooltip. The header
151
+ * row is dense (icon, name, kind tag, schema) so we deliberately
152
+ * pick the compact layout here rather than rendering pills.
153
+ */}
154
+ <DatabaseAnnotationDisplay
155
+ stereotypes={relation.stereotypes}
156
+ taggedValues={relation.taggedValues}
157
+ layout="compact"
158
+ />
159
+ </div>
160
+
161
+ <div className="database-diagram__table-node__columns">
162
+ {isViewKind
163
+ ? renderViewColumns(
164
+ relation as View,
165
+ viewColumnFormulas,
166
+ selectedViewColumnName,
167
+ )
168
+ : renderTableColumns(relation as Table, fkColumns, selectedColumn)}
169
+ </div>
170
+ {isViewKind &&
171
+ renderViewGroupBySection(relation as View, viewGroupByFormulas)}
172
+ </div>
173
+ );
174
+ },
175
+ );
176
+
177
+ // Function declarations (not const arrow) so they're hoisted and the React
178
+ // component above can call them without triggering `no-use-before-define`.
179
+ function renderTableColumns(
180
+ table: Table,
181
+ fkColumns: Set<Column>,
182
+ selectedColumn: Column | undefined,
183
+ ): ReactElement[] {
184
+ return getTableColumns(table).map((column) => {
185
+ const isPk = isPrimaryKey(table, column.name);
186
+ const isFk = fkColumns.has(column);
187
+ const isFocused = selectedColumn === column;
188
+ return (
189
+ <div
190
+ key={column.name}
191
+ className={clsx('database-diagram__table-node__column', {
192
+ 'database-diagram__table-node__column--pk': isPk,
193
+ 'database-diagram__table-node__column--fk': isFk,
194
+ 'database-diagram__table-node__column--nullable':
195
+ column.nullable === true,
196
+ 'database-diagram__table-node__column--focused': isFocused,
197
+ })}
198
+ title={column.nullable ? `${column.name} (nullable)` : column.name}
199
+ >
200
+ <div className="database-diagram__table-node__column__key">
201
+ {isPk ? <KeyIcon /> : null}
202
+ </div>
203
+ <div className="database-diagram__table-node__column__name">
204
+ {column.name}
205
+ </div>
206
+ <div className="database-diagram__table-node__column__type">
207
+ {getColumnTypeLabel(column)}
208
+ </div>
209
+ </div>
210
+ );
211
+ });
212
+ }
213
+
214
+ /**
215
+ * View columns are defined by `view.columnMappings` rather than `Column[]`.
216
+ * Each mapping carries the column name and a relational operation that
217
+ * computes the value. We render the Pure code returned by the engine (cached
218
+ * in `DatabaseEditorState.viewColumnFormulas`); while it's still loading we
219
+ * fall back to a placeholder so the layout stays stable.
220
+ *
221
+ * The `title` attribute also surfaces the formula for tooltip-on-hover, since
222
+ * complex formulas may not fit in the 1-line display.
223
+ */
224
+ function renderViewColumns(
225
+ view: View,
226
+ formulas: ReadonlyMap<string, string>,
227
+ selectedViewColumnName: string | undefined,
228
+ ): ReactElement[] {
229
+ return view.columnMappings.map((mapping) => {
230
+ const isPk = isPrimaryKey(view, mapping.columnName);
231
+ const isFocused = selectedViewColumnName === mapping.columnName;
232
+ const formula = resolveViewColumnFormula(
233
+ formulas,
234
+ view.schema.name,
235
+ view.name,
236
+ mapping.columnName,
237
+ );
238
+ return (
239
+ <div
240
+ key={mapping.columnName}
241
+ className={clsx('database-diagram__table-node__column', {
242
+ 'database-diagram__table-node__column--pk': isPk,
243
+ 'database-diagram__table-node__column--view': true,
244
+ 'database-diagram__table-node__column--focused': isFocused,
245
+ })}
246
+ title={`${mapping.columnName}: ${formula}`}
247
+ >
248
+ <div className="database-diagram__table-node__column__key">
249
+ {isPk ? <KeyIcon /> : null}
250
+ </div>
251
+ <div className="database-diagram__table-node__column__name">
252
+ {mapping.columnName}
253
+ </div>
254
+ <div className="database-diagram__table-node__column__type">
255
+ {formula}
256
+ </div>
257
+ </div>
258
+ );
259
+ });
260
+ }
261
+
262
+ /**
263
+ * Render the secondary view-metadata tags shown next to the primary "VIEW"
264
+ * tag in the canvas node header. These mirror the tags rendered in the
265
+ * schema tree's view row (see `DatabaseTreeViewRow`) so the two surfaces
266
+ * stay in sync.
267
+ *
268
+ * Returns `null` when the view has none of these features set, so the
269
+ * header stays compact for "plain" views.
270
+ */
271
+ function renderViewMetadataTags(view: View): ReactElement | null {
272
+ const groupByCount = view.groupBy?.columns.length ?? 0;
273
+ const hasDistinct = view.distinct === true;
274
+ const hasFilter = Boolean(view.filter);
275
+ const hasGroupBy = groupByCount > 0;
276
+ if (!hasDistinct && !hasFilter && !hasGroupBy) {
277
+ return null;
278
+ }
279
+ return (
280
+ <>
281
+ {hasDistinct && (
282
+ <div
283
+ className="database-diagram__table-node__header__kind-tag database-diagram__table-node__header__kind-tag--distinct"
284
+ title="View applies DISTINCT"
285
+ >
286
+ DISTINCT
287
+ </div>
288
+ )}
289
+ {hasFilter && view.filter && (
290
+ <div
291
+ className="database-diagram__table-node__header__kind-tag database-diagram__table-node__header__kind-tag--filtered"
292
+ title={`Filtered by ${
293
+ view.filter.filter.ownerReference.valueForSerialization ?? ''
294
+ }.${view.filter.filterName}`}
295
+ >
296
+ FILTERED
297
+ </div>
298
+ )}
299
+ {hasGroupBy && (
300
+ <div
301
+ className="database-diagram__table-node__header__kind-tag database-diagram__table-node__header__kind-tag--grouped"
302
+ title={`GROUP BY ${groupByCount} expression${groupByCount === 1 ? '' : 's'}`}
303
+ >
304
+ {`GROUP BY (${groupByCount})`}
305
+ </div>
306
+ )}
307
+ </>
308
+ );
309
+ }
310
+
311
+ /**
312
+ * Compact header tags surfacing a table's `milestoning` configuration.
313
+ * One tag per Milestoning entry (a table can declare both business and
314
+ * processing milestoning). The tag color follows the kind classifier from
315
+ * `summarizeMilestoning` so business and processing read distinctly.
316
+ *
317
+ * Returns `null` for non-milestoned tables to keep the header compact \u2014
318
+ * the vast majority of tables are not milestoned.
319
+ */
320
+ function renderMilestoningTags(table: Table): ReactElement | null {
321
+ if (table.milestoning.length === 0) {
322
+ return null;
323
+ }
324
+ return (
325
+ <>
326
+ {table.milestoning.map((milestoning) => {
327
+ const summary = summarizeMilestoning(milestoning);
328
+ return (
329
+ <div
330
+ // The label is content-derived (e.g. `business[from…thru]`)
331
+ // and is unique per milestoning declaration on a single table
332
+ // — the metamodel has no other stable identifier.
333
+ key={summary.label}
334
+ className={clsx(
335
+ 'database-diagram__table-node__header__kind-tag',
336
+ `database-diagram__table-node__header__kind-tag--milestoned-${summary.kind}`,
337
+ )}
338
+ title={summary.description}
339
+ >
340
+ {summary.label}
341
+ </div>
342
+ );
343
+ })}
344
+ </>
345
+ );
346
+ }
347
+
348
+ /**
349
+ * Footer rendered under the column rows when a view declares `groupBy`.
350
+ * Lists each grouping expression as one row of Pure code (lazy-resolved
351
+ * from `viewGroupByFormulas` — falls back to a placeholder while the
352
+ * batched engine call is in flight).
353
+ *
354
+ * Returns `null` for views with no groupBy and for table-kind nodes, so the
355
+ * footer doesn't add visual weight to nodes that don't need it.
356
+ */
357
+ function renderViewGroupBySection(
358
+ view: View,
359
+ formulas: ReadonlyMap<string, string>,
360
+ ): ReactElement | null {
361
+ const columns = view.groupBy?.columns ?? [];
362
+ if (columns.length === 0) {
363
+ return null;
364
+ }
365
+ return (
366
+ <div
367
+ className="database-diagram__table-node__group-by"
368
+ title={`GROUP BY (${columns.length})`}
369
+ >
370
+ <div className="database-diagram__table-node__group-by__header">
371
+ GROUP BY
372
+ </div>
373
+ {columns.map((_, index) => {
374
+ const formula = resolveViewGroupByFormula(
375
+ formulas,
376
+ view.schema.name,
377
+ view.name,
378
+ index,
379
+ );
380
+ return (
381
+ <div
382
+ // The resolved formula text is the only content-derived stable
383
+ // identity for groupBy expressions — the metamodel exposes
384
+ // them as an unnamed flat list.
385
+ key={formula}
386
+ className="database-diagram__table-node__group-by__row"
387
+ title={formula}
388
+ >
389
+ <span className="database-diagram__table-node__group-by__index">
390
+ {`[${index}]`}
391
+ </span>
392
+ <span className="database-diagram__table-node__group-by__formula">
393
+ {formula}
394
+ </span>
395
+ </div>
396
+ );
397
+ })}
398
+ </div>
399
+ );
400
+ }
401
+
402
+ /**
403
+ * Data attached to the placeholder node that stands in for a relation that
404
+ * lives in another database (cross-database join endpoint). Rendered
405
+ * smaller and visually distinct so users can tell at a glance that the
406
+ * actual relation isn't part of this database's schema tree.
407
+ */
408
+ export interface DatabaseForeignRelationStubNodeData
409
+ extends Record<string, unknown> {
410
+ schemaName: string;
411
+ relationName: string;
412
+ /** Path of the database that actually owns the relation. Surfaced in the
413
+ * tooltip so users know where to navigate to see the real definition. */
414
+ ownerPath: string;
415
+ isJoinEndpoint: boolean;
416
+ }
417
+
418
+ /**
419
+ * Compact stub rendered when a join references a relation in another
420
+ * database. Shows the schema/name plus the owning database path, with a
421
+ * dashed border to distinguish it from real in-database table nodes. Two
422
+ * invisible Handles let React Flow attach edges to either side. Not
423
+ * selectable — the canvas swallows clicks on stubs.
424
+ */
425
+ export const DatabaseForeignRelationStubNode = observer(
426
+ (props: NodeProps & { data: DatabaseForeignRelationStubNodeData }) => {
427
+ const { schemaName, relationName, ownerPath, isJoinEndpoint } = props.data;
428
+ return (
429
+ <div
430
+ className={clsx(
431
+ 'database-diagram__table-node',
432
+ 'database-diagram__table-node--foreign-stub',
433
+ {
434
+ 'database-diagram__table-node--join-endpoint': isJoinEndpoint,
435
+ },
436
+ )}
437
+ title={`${schemaName}.${relationName}\n(in database: ${ownerPath})`}
438
+ >
439
+ <Handle
440
+ type="target"
441
+ position={Position.Left}
442
+ className="database-diagram__table-node__handle"
443
+ isConnectable={false}
444
+ />
445
+ <Handle
446
+ type="source"
447
+ position={Position.Right}
448
+ className="database-diagram__table-node__handle"
449
+ isConnectable={false}
450
+ />
451
+ <div className="database-diagram__table-node__header">
452
+ <div className="database-diagram__table-node__header__icon">
453
+ <PURE_DatabaseIcon />
454
+ </div>
455
+ <div className="database-diagram__table-node__header__name">
456
+ {`${schemaName}.${relationName}`}
457
+ </div>
458
+ </div>
459
+ <div className="database-diagram__table-node__foreign-stub__owner">
460
+ {ownerPath}
461
+ </div>
462
+ </div>
463
+ );
464
+ },
465
+ );