@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,755 @@
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
+ import { action, computed, flow, flowResult, makeObservable, observable, } from 'mobx';
17
+ import { ElementEditorState } from './ElementEditorState.js';
18
+ import { LogEvent, assertErrorThrown, guaranteeType, noop, } from '@finos/legend-shared';
19
+ import { Database, GRAPH_MANAGER_EVENT, } from '@finos/legend-graph';
20
+ import { LegendStudioUserDataHelper } from '../../../../__lib__/LegendStudioUserDataHelper.js';
21
+ /**
22
+ * Top-level tabs inside the Database form-mode editor. `VIEW` shows the ERD
23
+ * canvas; `GRAMMAR` shows a read-only preview of the same Pure DSL grammar
24
+ * that the global Text Mode would render.
25
+ */
26
+ export var DATABASE_EDITOR_TAB;
27
+ (function (DATABASE_EDITOR_TAB) {
28
+ DATABASE_EDITOR_TAB["VIEW"] = "VIEW";
29
+ DATABASE_EDITOR_TAB["GRAMMAR"] = "GRAMMAR";
30
+ })(DATABASE_EDITOR_TAB || (DATABASE_EDITOR_TAB = {}));
31
+ /**
32
+ * Stable id for an element of the schema tree. We use a string id (rather than
33
+ * holding object references) so the side-panel's expand/collapse state can
34
+ * survive reprocessing without any cross-instance bookkeeping.
35
+ *
36
+ * Tables and Views share the same id namespace because they share the same
37
+ * `<schema>.<name>` qualifier — the metamodel doesn't allow a table and a view
38
+ * to collide on name within a schema.
39
+ */
40
+ export const getSchemaNodeId = (schemaName) => schemaName;
41
+ export const getRelationNodeId = (schemaName, relationName) => `${schemaName}.${relationName}`;
42
+ /**
43
+ * Stable key used to look up the Pure-code formula for a single view column
44
+ * inside `DatabaseEditorState.viewColumnFormulas`. Joins schema, view, and
45
+ * column so it survives reprocessing and can't collide across schemas.
46
+ */
47
+ export const getViewColumnFormulaKey = (schemaName, viewName, columnName) => `${schemaName}.${viewName}.${columnName}`;
48
+ /**
49
+ * Stable key used to look up the Pure-code formula for a single filter inside
50
+ * `DatabaseEditorState.filterFormulas`. Filter names are unique within a
51
+ * Database (they live as a flat `Database.filters: Filter[]`), so the name
52
+ * alone is sufficient — no schema qualifier needed.
53
+ */
54
+ export const getFilterFormulaKey = (filterName) => filterName;
55
+ /**
56
+ * Stable key used to look up the Pure-code formula for a single join. Join
57
+ * names are unique within a Database (they live as a flat
58
+ * `Database.joins: Join[]`), so the name alone is sufficient. Mirrors the
59
+ * filter helper above.
60
+ */
61
+ export const getJoinFormulaKey = (joinName) => joinName;
62
+ /**
63
+ * Stable key used to look up the Pure-code formula for a single view's
64
+ * groupBy column expression. Views are scoped to a schema and groupBy
65
+ * positions matter (the engine renders them in declaration order), so the
66
+ * key includes both the schema-qualified view name and the position index.
67
+ */
68
+ export const getViewGroupByFormulaKey = (schemaName, viewName, index) => `${schemaName}.${viewName}.groupBy[${index}]`;
69
+ /**
70
+ * Walk the V1-shaped Database entity content and collect every view's column
71
+ * mapping operations into a Map keyed by `<schema>.<view>.<column>`. The
72
+ * traversal mirrors the V1_Database / V1_Schema / V1_View / V1_ColumnMapping
73
+ * shape — anything that doesn't match that shape is skipped silently.
74
+ *
75
+ * We use loose typing because the entity content is `Record<PropertyKey,
76
+ * unknown>` and the V1 protocol types aren't exported from `@finos/legend-
77
+ * graph` for direct use here.
78
+ */
79
+ const collectRawViewColumnOperations = (content) => {
80
+ const out = new Map();
81
+ const dbContent = content;
82
+ if (!dbContent || !Array.isArray(dbContent.schemas)) {
83
+ return out;
84
+ }
85
+ for (const schemaJson of dbContent.schemas) {
86
+ const schema = schemaJson;
87
+ if (!schema || typeof schema.name !== 'string') {
88
+ continue;
89
+ }
90
+ const views = Array.isArray(schema.views) ? schema.views : [];
91
+ for (const viewJson of views) {
92
+ const view = viewJson;
93
+ if (!view || typeof view.name !== 'string') {
94
+ continue;
95
+ }
96
+ const mappings = Array.isArray(view.columnMappings)
97
+ ? view.columnMappings
98
+ : [];
99
+ for (const mappingJson of mappings) {
100
+ const mapping = mappingJson;
101
+ if (!mapping ||
102
+ typeof mapping.name !== 'string' ||
103
+ typeof mapping.operation !== 'object' ||
104
+ mapping.operation === null) {
105
+ continue;
106
+ }
107
+ out.set(getViewColumnFormulaKey(schema.name, view.name, mapping.name), mapping.operation);
108
+ }
109
+ }
110
+ }
111
+ return out;
112
+ };
113
+ /**
114
+ * Walk the V1-shaped Database entity content and collect every view's
115
+ * groupBy column expressions into a Map keyed by
116
+ * `<schema>.<view>.groupBy[<index>]`. The V1 shape is
117
+ * `schemas[].views[].groupBy.columns[]: RawRelationalOperationElement`.
118
+ * Anything that doesn't match that shape is skipped silently. Same loose
119
+ * typing as the column-mapping walker above.
120
+ */
121
+ const collectRawViewGroupByOperations = (content) => {
122
+ const out = new Map();
123
+ const dbContent = content;
124
+ if (!dbContent || !Array.isArray(dbContent.schemas)) {
125
+ return out;
126
+ }
127
+ for (const schemaJson of dbContent.schemas) {
128
+ const schema = schemaJson;
129
+ if (!schema || typeof schema.name !== 'string') {
130
+ continue;
131
+ }
132
+ const views = Array.isArray(schema.views) ? schema.views : [];
133
+ for (const viewJson of views) {
134
+ // V1 protocol stores `View.groupBy` as a flat array of operation
135
+ // elements (see `V1_View.groupBy: V1_RelationalOperationElement[]`),
136
+ // NOT wrapped in `{ columns: [...] }` like the metamodel side. The
137
+ // serializer drops the field entirely when there are no group-by
138
+ // columns, so `view.groupBy` may legitimately be undefined.
139
+ const view = viewJson;
140
+ if (!view ||
141
+ typeof view.name !== 'string' ||
142
+ !Array.isArray(view.groupBy)) {
143
+ continue;
144
+ }
145
+ view.groupBy.forEach((opJson, index) => {
146
+ if (typeof opJson !== 'object' || opJson === null) {
147
+ return;
148
+ }
149
+ out.set(getViewGroupByFormulaKey(schema.name, view.name, index), opJson);
150
+ });
151
+ }
152
+ }
153
+ return out;
154
+ };
155
+ /**
156
+ * Walk the V1-shaped Database entity content and collect every filter's
157
+ * operation into a Map keyed by filter name. Filters live at the database
158
+ * level (a flat `filters[]` on the V1 root), unlike views which are nested
159
+ * inside schemas — so this walker is shallower than
160
+ * `collectRawViewColumnOperations`.
161
+ *
162
+ * Anything that doesn't match the expected `{ name, operation }` shape is
163
+ * skipped silently. Loose typing for the same reason as the view walker:
164
+ * entity content is `Record<PropertyKey, unknown>`.
165
+ */
166
+ const collectRawFilterOperations = (content) => {
167
+ const out = new Map();
168
+ const dbContent = content;
169
+ if (!dbContent || !Array.isArray(dbContent.filters)) {
170
+ return out;
171
+ }
172
+ for (const filterJson of dbContent.filters) {
173
+ const filter = filterJson;
174
+ if (!filter ||
175
+ typeof filter.name !== 'string' ||
176
+ typeof filter.operation !== 'object' ||
177
+ filter.operation === null) {
178
+ continue;
179
+ }
180
+ out.set(getFilterFormulaKey(filter.name), filter.operation);
181
+ }
182
+ return out;
183
+ };
184
+ /**
185
+ * Walk the V1-shaped Database entity content and collect every join's
186
+ * operation into a Map keyed by join name. Joins live at the database level
187
+ * (a flat `joins[]` on the V1 root) under the shape `{ name, operation }`,
188
+ * mirroring filters. Anything that doesn't match the expected shape is
189
+ * skipped silently.
190
+ */
191
+ const collectRawJoinOperations = (content) => {
192
+ const out = new Map();
193
+ const dbContent = content;
194
+ if (!dbContent || !Array.isArray(dbContent.joins)) {
195
+ return out;
196
+ }
197
+ for (const joinJson of dbContent.joins) {
198
+ const join = joinJson;
199
+ if (!join ||
200
+ typeof join.name !== 'string' ||
201
+ typeof join.operation !== 'object' ||
202
+ join.operation === null) {
203
+ continue;
204
+ }
205
+ out.set(getJoinFormulaKey(join.name), join.operation);
206
+ }
207
+ return out;
208
+ };
209
+ /**
210
+ * View-only form mode for `Database` elements. Renders an ERD-style canvas
211
+ * (tables as nodes, joins as edges) plus a side-panel tree of schemas/tables/
212
+ * columns. A second tab shows the same grammar that Text Mode would show.
213
+ *
214
+ * Layout positions are derived per-render via dagre and not persisted yet —
215
+ * persistence + edit support are intended follow-ups.
216
+ */
217
+ export class DatabaseEditorState extends ElementEditorState {
218
+ selectedTab = DATABASE_EDITOR_TAB.VIEW;
219
+ // ---- Selection -----------------------------------------------------------
220
+ // Mutually exclusive selection axes:
221
+ // - `selectedRelation` (+ optional `selectedColumn`): a Table or View is
222
+ // the focus. Drives the blue ring on the canvas node and side-panel row.
223
+ // - `selectedJoin`: a join is the focus. Drives the yellow edge style on
224
+ // the canvas and yellow rings on both endpoint relations ("join
225
+ // endpoints"), plus the side-panel join row highlight.
226
+ // - `selectedFilter`: a database-level filter is the focus. Filters don't
227
+ // have a canvas representation in the MVP — they live in the side panel
228
+ // only — so this only drives the side-panel row highlight.
229
+ // Setting one clears the others — see action implementations below.
230
+ //
231
+ // `selectedColumn` is only meaningful when `selectedRelation` is a Table —
232
+ // for views, column-mappings aren't `Column` instances and we don't support
233
+ // per-mapping highlight in the MVP.
234
+ selectedRelation;
235
+ selectedColumn;
236
+ selectedJoin;
237
+ selectedFilter;
238
+ // For views (which have `columnMappings`, not `Column` instances), column
239
+ // selection is tracked by name. Mutually exclusive with `selectedColumn`
240
+ // — a relation is either a Table (use `selectedColumn`) or a View (use
241
+ // `selectedViewColumnName`); never both. Always `undefined` when the
242
+ // selected relation is a Table.
243
+ selectedViewColumnName;
244
+ // ---- Side-panel expansion -----------------------------------------------
245
+ // Schemas default to expanded so users immediately see their relations;
246
+ // tables/views default to collapsed so the tree isn't overwhelming on
247
+ // large databases.
248
+ expandedSchemaIds = new Set();
249
+ expandedRelationIds = new Set();
250
+ // ---- Pan-to-selected trigger --------------------------------------------
251
+ // Selecting a row in the side panel should pan the canvas to that table;
252
+ // selecting a node directly on the canvas should NOT (the user is already
253
+ // looking at it). Rather than coupling the two components through callbacks,
254
+ // we increment this counter on side-panel actions and let the canvas
255
+ // observe it via `useEffect`.
256
+ panToSelectedRequestCounter = 0;
257
+ // ---- Canvas action triggers --------------------------------------------
258
+ // Same counter pattern as `panToSelectedRequestCounter` for one-shot
259
+ // actions the side-panel header / canvas toolbar fire and the canvas
260
+ // executes. Counters (rather than booleans) so identical successive
261
+ // requests still trigger.
262
+ fitAllRequestCounter = 0;
263
+ resetLayoutRequestCounter = 0;
264
+ // ---- Tree search --------------------------------------------------------
265
+ // User-entered filter applied to the schema tree (schema / table / view /
266
+ // column names). Empty string = no filter. Lowercase comparison is done
267
+ // at the consumer side via `searchTextLowerCase` so we don't pay the
268
+ // toLowerCase cost in every row render.
269
+ searchText = '';
270
+ // ---- View-column Pure-code formulas -------------------------------------
271
+ // Populated lazily by `loadViewColumnFormulas()` (one batched engine call
272
+ // per Database load). Until populated — or for column mappings the engine
273
+ // can't render — the UI falls back to the static placeholder
274
+ // ("calculate [...]") so views always render something useful.
275
+ // Keyed by `getViewColumnFormulaKey(schema, view, column)`.
276
+ viewColumnFormulas = new Map();
277
+ isLoadingViewColumnFormulas = false;
278
+ // ---- Filter Pure-code formulas ------------------------------------------
279
+ // Same pattern as `viewColumnFormulas` but for `Database.filters[].operation`.
280
+ // Populated lazily by `loadFilterFormulas()` in a single batched engine call.
281
+ // Keyed by filter name (filters are unique within a database).
282
+ filterFormulas = new Map();
283
+ isLoadingFilterFormulas = false;
284
+ // ---- Join Pure-code formulas --------------------------------------------
285
+ // Same pattern as `viewColumnFormulas` and `filterFormulas` but for
286
+ // `Database.joins[].operation`. Populated lazily by `loadJoinFormulas()` in
287
+ // a single batched engine call. Surfaced in the side panel under each join
288
+ // row and in the canvas "selected join" floating card.
289
+ joinFormulas = new Map();
290
+ isLoadingJoinFormulas = false;
291
+ // ---- View groupBy Pure-code formulas ------------------------------------
292
+ // Same pattern as `viewColumnFormulas` but for `View.groupBy.columns`.
293
+ // Populated lazily by `loadViewGroupByFormulas()` in a single batched
294
+ // engine call. Keyed by `<schema>.<view>.groupBy[<index>]` so positional
295
+ // order is preserved (groupBy expressions are positional).
296
+ viewGroupByFormulas = new Map();
297
+ isLoadingViewGroupByFormulas = false;
298
+ // ---- Layout -------------------------------------------------------------
299
+ // Side-panel (schema tree) is user-resizable on the canvas. We keep its
300
+ // collapsed state on the editor state so it survives tab switches and so
301
+ // a future toggle button outside the panel can drive it. Width is owned
302
+ // by `react-reflex` and not tracked here — only the binary collapse flag.
303
+ isSidePanelCollapsed = false;
304
+ // ---- Theme --------------------------------------------------------------
305
+ // The wider Studio app is dark-mode-only today. We allow this editor (and
306
+ // only this editor) to opt into a light theme via a toolbar toggle. The
307
+ // setting lives on the editor state so it survives tab switches and
308
+ // recompiles within the same session, and is persisted per-user via
309
+ // `UserDataService` (localStorage) so the choice survives reloads.
310
+ // TODO: when Studio adopts app-wide theming via `LayoutService` (Query
311
+ // already does this with `setColorTheme(..., { persist: true })`), drop
312
+ // this local observable + persistence and react to
313
+ // `applicationStore.layoutService.currentColorTheme` instead so this
314
+ // editor stays in sync with the rest of the app.
315
+ theme = 'dark';
316
+ constructor(editorStore, element) {
317
+ super(editorStore, element);
318
+ makeObservable(this, {
319
+ selectedTab: observable,
320
+ selectedRelation: observable,
321
+ selectedColumn: observable,
322
+ selectedViewColumnName: observable,
323
+ selectedJoin: observable,
324
+ selectedFilter: observable,
325
+ expandedSchemaIds: observable,
326
+ expandedRelationIds: observable,
327
+ panToSelectedRequestCounter: observable,
328
+ fitAllRequestCounter: observable,
329
+ resetLayoutRequestCounter: observable,
330
+ searchText: observable,
331
+ viewColumnFormulas: observable,
332
+ isLoadingViewColumnFormulas: observable,
333
+ filterFormulas: observable,
334
+ isLoadingFilterFormulas: observable,
335
+ joinFormulas: observable,
336
+ isLoadingJoinFormulas: observable,
337
+ viewGroupByFormulas: observable,
338
+ isLoadingViewGroupByFormulas: observable,
339
+ isSidePanelCollapsed: observable,
340
+ theme: observable,
341
+ database: computed,
342
+ setSelectedTab: action,
343
+ setSelectedRelation: action,
344
+ focusOnRelation: action,
345
+ focusOnColumn: action,
346
+ focusOnViewColumn: action,
347
+ focusOnJoin: action,
348
+ focusOnFilter: action,
349
+ clearSelection: action,
350
+ toggleSchemaExpanded: action,
351
+ toggleRelationExpanded: action,
352
+ expandAllSchemas: action,
353
+ collapseAll: action,
354
+ setSidePanelCollapsed: action,
355
+ toggleSidePanelCollapsed: action,
356
+ toggleTheme: action,
357
+ setSearchText: action,
358
+ requestFitAll: action,
359
+ requestResetLayout: action,
360
+ generateGrammarText: flow,
361
+ loadViewColumnFormulas: flow,
362
+ loadFilterFormulas: flow,
363
+ loadJoinFormulas: flow,
364
+ loadViewGroupByFormulas: flow,
365
+ });
366
+ // Default: every schema starts expanded so the tree is immediately useful.
367
+ this.database.schemas.forEach((schema) => {
368
+ this.expandedSchemaIds.add(getSchemaNodeId(schema.name));
369
+ });
370
+ // Hydrate the persisted theme preference (if any). Falls through to the
371
+ // default 'dark' when the user has never set it. Done synchronously in
372
+ // the constructor so the first render already reflects the stored choice.
373
+ const persistedTheme = LegendStudioUserDataHelper.databaseEditor_getTheme(this.editorStore.applicationStore.userDataService);
374
+ if (persistedTheme) {
375
+ this.theme = persistedTheme;
376
+ }
377
+ // Kick off the formula load eagerly. The flow is async and writes into
378
+ // `viewColumnFormulas` when ready — components render with the placeholder
379
+ // until then, so the UI is never blocked on this. Errors are logged and
380
+ // swallowed; the placeholder remains.
381
+ flowResult(this.loadViewColumnFormulas()).catch(noop());
382
+ // Same for filter formulas — independent batched engine call so the two
383
+ // loads run in parallel.
384
+ flowResult(this.loadFilterFormulas()).catch(noop());
385
+ // And join formulas — third independent batched engine call. All three
386
+ // are fire-and-forget on construction; consumers fall back to the
387
+ // placeholder text until they resolve.
388
+ flowResult(this.loadJoinFormulas()).catch(noop());
389
+ // And view-groupBy formulas — fourth independent batched engine call.
390
+ // Only fires for databases that actually have at least one view with a
391
+ // groupBy; the loader bails out otherwise.
392
+ flowResult(this.loadViewGroupByFormulas()).catch(noop());
393
+ }
394
+ get database() {
395
+ return guaranteeType(this.element, Database, 'Element inside database editor state must be a Database');
396
+ }
397
+ // -------------------------------------------------------------------------
398
+ // Tab navigation
399
+ // -------------------------------------------------------------------------
400
+ setSelectedTab(tab) {
401
+ this.selectedTab = tab;
402
+ if (tab === DATABASE_EDITOR_TAB.GRAMMAR) {
403
+ // Lazily regenerate so the grammar preview always matches the current
404
+ // metamodel state. Errors are swallowed — the flow itself writes a
405
+ // diagnostic comment into `textContent` on failure.
406
+ flowResult(this.generateGrammarText()).catch(noop());
407
+ }
408
+ }
409
+ /**
410
+ * Generate the Pure grammar for the underlying Database element and store
411
+ * it in `textContent`. Mirrors `generateElementGrammar()` from the base
412
+ * class but is kept local so the consumer doesn't need to know about the
413
+ * inherited flow.
414
+ */
415
+ *generateGrammarText() {
416
+ yield flowResult(this.generateElementGrammar());
417
+ }
418
+ /**
419
+ * Render every view column-mapping's relational operation as Pure code, in
420
+ * a single batched engine call.
421
+ *
422
+ * Strategy: rather than transform metamodel `RelationalOperationElement`
423
+ * instances by hand (the V1 transformer isn't a public export), we rely on
424
+ * `elementToEntity(database)` which serializes the Database to its V1 JSON
425
+ * form synchronously. The resulting `entity.content` already contains the
426
+ * raw operations under
427
+ * `schemas[].views[].columnMappings[].operation`
428
+ * — exactly the shape `relationalOperationElementToPureCode` expects. We
429
+ * walk that JSON, build a Map keyed by `<schema>.<view>.<column>`, and let
430
+ * the engine return the rendered Pure code.
431
+ *
432
+ * On any failure (network/server/serialization), the partial map (possibly
433
+ * empty) stays in place and consumers fall back to the placeholder.
434
+ */
435
+ *loadViewColumnFormulas() {
436
+ // Skip the round-trip entirely if the database has no views.
437
+ const hasAnyView = this.database.schemas.some((schema) => schema.views.length > 0);
438
+ if (!hasAnyView) {
439
+ return;
440
+ }
441
+ this.isLoadingViewColumnFormulas = true;
442
+ try {
443
+ const entity = this.editorStore.graphManagerState.graphManager.elementToEntity(this.database, { pruneSourceInformation: true });
444
+ const operations = collectRawViewColumnOperations(entity.content);
445
+ if (operations.size === 0) {
446
+ return;
447
+ }
448
+ const rendered = (yield this.editorStore.graphManagerState.graphManager.relationalOperationElementToPureCode(operations));
449
+ // Replace the whole map atomically so consumers (which read it as a
450
+ // computed dependency) re-render once.
451
+ this.viewColumnFormulas = new Map(rendered);
452
+ }
453
+ catch (error) {
454
+ assertErrorThrown(error);
455
+ this.editorStore.applicationStore.logService.error(LogEvent.create(GRAPH_MANAGER_EVENT.PARSING_FAILURE), `Couldn't render view-column formulas for database ${this.database.path}`, error);
456
+ }
457
+ finally {
458
+ this.isLoadingViewColumnFormulas = false;
459
+ }
460
+ }
461
+ /**
462
+ * Render every database-level filter's relational operation as Pure code, in
463
+ * a single batched engine call.
464
+ *
465
+ * Same strategy as `loadViewColumnFormulas`: serialize the database to its
466
+ * V1 JSON via `elementToEntity` and walk the `filters[]` array on the root
467
+ * for `{ name, operation }` pairs. Filter names are unique within a
468
+ * Database, so the lookup key is just the filter name.
469
+ *
470
+ * On any failure (network/server/serialization), the partial map (possibly
471
+ * empty) stays in place and consumers fall back to the placeholder.
472
+ */
473
+ *loadFilterFormulas() {
474
+ // Skip the round-trip entirely if the database has no filters.
475
+ if (this.database.filters.length === 0) {
476
+ return;
477
+ }
478
+ this.isLoadingFilterFormulas = true;
479
+ try {
480
+ const entity = this.editorStore.graphManagerState.graphManager.elementToEntity(this.database, { pruneSourceInformation: true });
481
+ const operations = collectRawFilterOperations(entity.content);
482
+ if (operations.size === 0) {
483
+ return;
484
+ }
485
+ const rendered = (yield this.editorStore.graphManagerState.graphManager.relationalOperationElementToPureCode(operations));
486
+ // Replace the whole map atomically so consumers (which read it as a
487
+ // computed dependency) re-render once.
488
+ this.filterFormulas = new Map(rendered);
489
+ }
490
+ catch (error) {
491
+ assertErrorThrown(error);
492
+ this.editorStore.applicationStore.logService.error(LogEvent.create(GRAPH_MANAGER_EVENT.PARSING_FAILURE), `Couldn't render filter formulas for database ${this.database.path}`, error);
493
+ }
494
+ finally {
495
+ this.isLoadingFilterFormulas = false;
496
+ }
497
+ }
498
+ /**
499
+ * Render every join's relational operation as Pure code, in a single
500
+ * batched engine call. Same strategy as `loadFilterFormulas`: serialize
501
+ * the database to its V1 JSON via `elementToEntity` and walk the
502
+ * `joins[]` array on the root for `{ name, operation }` pairs. Join
503
+ * names are unique within a Database, so the lookup key is just the
504
+ * join name.
505
+ *
506
+ * On any failure (network/server/serialization), the partial map
507
+ * (possibly empty) stays in place and consumers fall back to the
508
+ * placeholder.
509
+ */
510
+ *loadJoinFormulas() {
511
+ if (this.database.joins.length === 0) {
512
+ return;
513
+ }
514
+ this.isLoadingJoinFormulas = true;
515
+ try {
516
+ const entity = this.editorStore.graphManagerState.graphManager.elementToEntity(this.database, { pruneSourceInformation: true });
517
+ const operations = collectRawJoinOperations(entity.content);
518
+ if (operations.size === 0) {
519
+ return;
520
+ }
521
+ const rendered = (yield this.editorStore.graphManagerState.graphManager.relationalOperationElementToPureCode(operations));
522
+ this.joinFormulas = new Map(rendered);
523
+ }
524
+ catch (error) {
525
+ assertErrorThrown(error);
526
+ this.editorStore.applicationStore.logService.error(LogEvent.create(GRAPH_MANAGER_EVENT.PARSING_FAILURE), `Couldn't render join formulas for database ${this.database.path}`, error);
527
+ }
528
+ finally {
529
+ this.isLoadingJoinFormulas = false;
530
+ }
531
+ }
532
+ /**
533
+ * Render every view's groupBy column expressions as Pure code, in a
534
+ * single batched engine call. Same strategy as `loadViewColumnFormulas`:
535
+ * serialize the database to its V1 JSON via `elementToEntity` and walk
536
+ * the `schemas[].views[].groupBy.columns[]` arrays for raw operations.
537
+ *
538
+ * Skips the round-trip entirely when no view declares a groupBy. On any
539
+ * failure the partial map (possibly empty) stays in place and consumers
540
+ * fall back to the static placeholder.
541
+ */
542
+ *loadViewGroupByFormulas() {
543
+ const hasAnyGroupBy = this.database.schemas.some((schema) => schema.views.some((view) => Boolean(view.groupBy)));
544
+ if (!hasAnyGroupBy) {
545
+ return;
546
+ }
547
+ this.isLoadingViewGroupByFormulas = true;
548
+ try {
549
+ const entity = this.editorStore.graphManagerState.graphManager.elementToEntity(this.database, { pruneSourceInformation: true });
550
+ const operations = collectRawViewGroupByOperations(entity.content);
551
+ if (operations.size === 0) {
552
+ return;
553
+ }
554
+ const rendered = (yield this.editorStore.graphManagerState.graphManager.relationalOperationElementToPureCode(operations));
555
+ this.viewGroupByFormulas = new Map(rendered);
556
+ }
557
+ catch (error) {
558
+ assertErrorThrown(error);
559
+ this.editorStore.applicationStore.logService.error(LogEvent.create(GRAPH_MANAGER_EVENT.PARSING_FAILURE), `Couldn't render view-groupBy formulas for database ${this.database.path}`, error);
560
+ }
561
+ finally {
562
+ this.isLoadingViewGroupByFormulas = false;
563
+ }
564
+ }
565
+ // -------------------------------------------------------------------------
566
+ // Selection
567
+ // -------------------------------------------------------------------------
568
+ /**
569
+ * Plain relation selection — used when the user clicks directly on a node
570
+ * on the canvas. Doesn't trigger a pan because the user is already looking
571
+ * at the node they clicked. Clears any active join/filter selection
572
+ * (selecting a relation moves us out of those focus modes).
573
+ */
574
+ setSelectedRelation(relation) {
575
+ this.selectedRelation = relation;
576
+ this.selectedColumn = undefined;
577
+ this.selectedViewColumnName = undefined;
578
+ this.selectedJoin = undefined;
579
+ this.selectedFilter = undefined;
580
+ }
581
+ /**
582
+ * Side-panel relation click — selects the relation AND requests a pan so
583
+ * the canvas centers on it. Works identically for tables and views.
584
+ */
585
+ focusOnRelation(relation) {
586
+ this.selectedRelation = relation;
587
+ this.selectedColumn = undefined;
588
+ this.selectedViewColumnName = undefined;
589
+ this.selectedJoin = undefined;
590
+ this.selectedFilter = undefined;
591
+ this.panToSelectedRequestCounter++;
592
+ }
593
+ /**
594
+ * Side-panel column click — selects the parent table, the specific column,
595
+ * and requests a pan. Inside the table-node component this drives the
596
+ * single-row highlight. Only meaningful for tables; view column-mappings
597
+ * use `focusOnRelation` for now.
598
+ */
599
+ focusOnColumn(table, column) {
600
+ this.selectedRelation = table;
601
+ this.selectedColumn = column;
602
+ this.selectedViewColumnName = undefined;
603
+ this.selectedJoin = undefined;
604
+ this.selectedFilter = undefined;
605
+ this.panToSelectedRequestCounter++;
606
+ }
607
+ /**
608
+ * Side-panel view column-mapping click \u2014 selects the parent view AND
609
+ * the specific column-mapping by name (views don't have `Column` refs).
610
+ * Drives the per-row highlight inside the view's canvas node, mirroring
611
+ * how `focusOnColumn` works for tables.
612
+ */
613
+ focusOnViewColumn(view, columnMappingName) {
614
+ this.selectedRelation = view;
615
+ this.selectedColumn = undefined;
616
+ this.selectedViewColumnName = columnMappingName;
617
+ this.selectedJoin = undefined;
618
+ this.selectedFilter = undefined;
619
+ this.panToSelectedRequestCounter++;
620
+ }
621
+ /**
622
+ * Side-panel join click (or canvas edge click) — selects the join and
623
+ * requests a pan that fits BOTH endpoint relations in view. We clear the
624
+ * relation/column selection because join-mode is its own focus state with
625
+ * its own visual treatment (yellow rings on the two endpoints rather than
626
+ * a single blue ring on one).
627
+ */
628
+ focusOnJoin(join) {
629
+ this.selectedRelation = undefined;
630
+ this.selectedColumn = undefined;
631
+ this.selectedViewColumnName = undefined;
632
+ this.selectedJoin = join;
633
+ this.selectedFilter = undefined;
634
+ this.panToSelectedRequestCounter++;
635
+ }
636
+ /**
637
+ * Side-panel filter click — selects the filter. Filters don't have a
638
+ * canvas representation in the MVP (they live at the database level rather
639
+ * than tied to a specific table/edge), so this only highlights the row in
640
+ * the side panel; no pan is requested. Other selection axes are cleared so
641
+ * the highlight states stay mutually exclusive.
642
+ */
643
+ focusOnFilter(filter) {
644
+ this.selectedRelation = undefined;
645
+ this.selectedColumn = undefined;
646
+ this.selectedViewColumnName = undefined;
647
+ this.selectedJoin = undefined;
648
+ this.selectedFilter = filter;
649
+ }
650
+ /**
651
+ * Clear all selections. Called from the canvas pane click handler so that
652
+ * clicking empty space deselects everything regardless of which selection
653
+ * axis is active.
654
+ */
655
+ clearSelection() {
656
+ this.selectedRelation = undefined;
657
+ this.selectedColumn = undefined;
658
+ this.selectedViewColumnName = undefined;
659
+ this.selectedJoin = undefined;
660
+ this.selectedFilter = undefined;
661
+ }
662
+ // -------------------------------------------------------------------------
663
+ // Expand/collapse
664
+ // -------------------------------------------------------------------------
665
+ toggleSchemaExpanded(id) {
666
+ if (this.expandedSchemaIds.has(id)) {
667
+ this.expandedSchemaIds.delete(id);
668
+ }
669
+ else {
670
+ this.expandedSchemaIds.add(id);
671
+ }
672
+ }
673
+ toggleRelationExpanded(id) {
674
+ if (this.expandedRelationIds.has(id)) {
675
+ this.expandedRelationIds.delete(id);
676
+ }
677
+ else {
678
+ this.expandedRelationIds.add(id);
679
+ }
680
+ }
681
+ expandAllSchemas() {
682
+ this.database.schemas.forEach((schema) => {
683
+ this.expandedSchemaIds.add(getSchemaNodeId(schema.name));
684
+ });
685
+ }
686
+ collapseAll() {
687
+ this.expandedSchemaIds.clear();
688
+ this.expandedRelationIds.clear();
689
+ }
690
+ /**
691
+ * Set the side panel's collapsed state directly. Useful for syncing the
692
+ * state when the user drags the splitter all the way down (i.e., the
693
+ * panel sets itself to collapsed in response to a resize event).
694
+ */
695
+ setSidePanelCollapsed(collapsed) {
696
+ this.isSidePanelCollapsed = collapsed;
697
+ }
698
+ /**
699
+ * Flip the side panel's collapsed state. Driven by the explicit toggle
700
+ * button in the panel header (and the chevron rendered when collapsed).
701
+ */
702
+ toggleSidePanelCollapsed() {
703
+ this.isSidePanelCollapsed = !this.isSidePanelCollapsed;
704
+ }
705
+ /**
706
+ * Flip the editor's local theme between dark (the Studio default) and
707
+ * light. Scoped to this editor only — the rest of Studio remains in its
708
+ * configured theme. The toggle is exposed via a button in the editor's
709
+ * tab header. The new value is persisted to `UserDataService` so the
710
+ * choice survives reloads.
711
+ */
712
+ toggleTheme() {
713
+ this.theme = this.theme === 'dark' ? 'light' : 'dark';
714
+ LegendStudioUserDataHelper.databaseEditor_setTheme(this.editorStore.applicationStore.userDataService, this.theme);
715
+ }
716
+ /**
717
+ * Set the tree search/filter text. Empty string means \u201cno filter\u201d.
718
+ * The schema-tree consumer derives a lowercase form per render rather
719
+ * than computing it here so identical successive sets stay cheap.
720
+ */
721
+ setSearchText(text) {
722
+ this.searchText = text;
723
+ }
724
+ /**
725
+ * Bump the fit-all counter. The canvas observes this and runs
726
+ * `fitView()` over the full graph (no `nodes` filter).
727
+ */
728
+ requestFitAll() {
729
+ this.fitAllRequestCounter++;
730
+ }
731
+ /**
732
+ * Bump the reset-layout counter. The canvas observes this and re-runs
733
+ * dagre over the current nodes/edges, undoing any user-initiated drags.
734
+ */
735
+ requestResetLayout() {
736
+ this.resetLayoutRequestCounter++;
737
+ }
738
+ reprocess(newElement, editorStore) {
739
+ const next = new DatabaseEditorState(editorStore, newElement);
740
+ // Preserve UX state across recompiles — a recompile shouldn't snap users
741
+ // back to the View tab or collapse everything.
742
+ next.selectedTab = this.selectedTab;
743
+ next.expandedSchemaIds = new Set(this.expandedSchemaIds);
744
+ next.expandedRelationIds = new Set(this.expandedRelationIds);
745
+ next.isSidePanelCollapsed = this.isSidePanelCollapsed;
746
+ next.theme = this.theme;
747
+ // Note: `viewColumnFormulas` and `filterFormulas` deliberately do NOT
748
+ // carry over. They're derived from operations on the new element and may
749
+ // have changed; the constructor's `loadViewColumnFormulas()` and
750
+ // `loadFilterFormulas()` kickoffs will refresh them. Until those resolve
751
+ // the placeholder shows briefly.
752
+ return next;
753
+ }
754
+ }
755
+ //# sourceMappingURL=DatabaseEditorState.js.map