@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.
- package/lib/__lib__/LegendStudioUserDataHelper.d.ts +4 -1
- package/lib/__lib__/LegendStudioUserDataHelper.d.ts.map +1 -1
- package/lib/__lib__/LegendStudioUserDataHelper.js +18 -0
- package/lib/__lib__/LegendStudioUserDataHelper.js.map +1 -1
- package/lib/components/editor/editor-group/EditorGroup.d.ts.map +1 -1
- package/lib/components/editor/editor-group/EditorGroup.js +5 -0
- package/lib/components/editor/editor-group/EditorGroup.js.map +1 -1
- package/lib/components/editor/editor-group/dataProduct/DataProductEditor.d.ts.map +1 -1
- package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js +13 -44
- package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js.map +1 -1
- package/lib/components/editor/editor-group/database-editor/DatabaseAnnotationDisplay.d.ts +26 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseAnnotationDisplay.d.ts.map +1 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseAnnotationDisplay.js +101 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseAnnotationDisplay.js.map +1 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseDiagramCanvas.d.ts +23 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseDiagramCanvas.d.ts.map +1 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseDiagramCanvas.js +434 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseDiagramCanvas.js.map +1 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseDiagramHelper.d.ts +242 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseDiagramHelper.d.ts.map +1 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseDiagramHelper.js +371 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseDiagramHelper.js.map +1 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseEditor.d.ts +29 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseEditor.d.ts.map +1 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseEditor.js +78 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseEditor.js.map +1 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseSchemaTree.d.ts +30 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseSchemaTree.d.ts.map +1 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseSchemaTree.js +331 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseSchemaTree.js.map +1 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseTableNode.d.ts +104 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseTableNode.d.ts.map +1 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseTableNode.js +151 -0
- package/lib/components/editor/editor-group/database-editor/DatabaseTableNode.js.map +1 -0
- package/lib/components/editor/editor-group/ingest-editor/IngestDefinitionEditor.d.ts.map +1 -1
- package/lib/components/editor/editor-group/ingest-editor/IngestDefinitionEditor.js +3 -78
- package/lib/components/editor/editor-group/ingest-editor/IngestDefinitionEditor.js.map +1 -1
- package/lib/index.css +2 -2
- package/lib/index.css.map +1 -1
- package/lib/package.json +4 -1
- package/lib/stores/editor/EditorTabManagerState.d.ts.map +1 -1
- package/lib/stores/editor/EditorTabManagerState.js +5 -3
- package/lib/stores/editor/EditorTabManagerState.js.map +1 -1
- package/lib/stores/editor/editor-state/element-editor-state/DatabaseEditorState.d.ts +252 -0
- package/lib/stores/editor/editor-state/element-editor-state/DatabaseEditorState.d.ts.map +1 -0
- package/lib/stores/editor/editor-state/element-editor-state/DatabaseEditorState.js +755 -0
- package/lib/stores/editor/editor-state/element-editor-state/DatabaseEditorState.js.map +1 -0
- package/package.json +12 -9
- package/src/__lib__/LegendStudioUserDataHelper.ts +30 -0
- package/src/components/editor/editor-group/EditorGroup.tsx +4 -0
- package/src/components/editor/editor-group/dataProduct/DataProductEditor.tsx +0 -52
- package/src/components/editor/editor-group/database-editor/DatabaseAnnotationDisplay.tsx +200 -0
- package/src/components/editor/editor-group/database-editor/DatabaseDiagramCanvas.tsx +701 -0
- package/src/components/editor/editor-group/database-editor/DatabaseDiagramHelper.ts +555 -0
- package/src/components/editor/editor-group/database-editor/DatabaseEditor.tsx +246 -0
- package/src/components/editor/editor-group/database-editor/DatabaseSchemaTree.tsx +1053 -0
- package/src/components/editor/editor-group/database-editor/DatabaseTableNode.tsx +465 -0
- package/src/components/editor/editor-group/ingest-editor/IngestDefinitionEditor.tsx +2 -242
- package/src/stores/editor/EditorTabManagerState.ts +4 -5
- package/src/stores/editor/editor-state/element-editor-state/DatabaseEditorState.ts +938 -0
- 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
|
+
);
|