@finos/legend-application-studio 28.21.4 → 28.21.6

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 (100) hide show
  1. package/lib/__lib__/LegendStudioEvent.d.ts +4 -1
  2. package/lib/__lib__/LegendStudioEvent.d.ts.map +1 -1
  3. package/lib/__lib__/LegendStudioEvent.js +3 -0
  4. package/lib/__lib__/LegendStudioEvent.js.map +1 -1
  5. package/lib/__lib__/LegendStudioTelemetryHelper.d.ts +2 -1
  6. package/lib/__lib__/LegendStudioTelemetryHelper.d.ts.map +1 -1
  7. package/lib/__lib__/LegendStudioTelemetryHelper.js +11 -3
  8. package/lib/__lib__/LegendStudioTelemetryHelper.js.map +1 -1
  9. package/lib/__lib__/LegendStudioUserDataHelper.d.ts +41 -1
  10. package/lib/__lib__/LegendStudioUserDataHelper.d.ts.map +1 -1
  11. package/lib/__lib__/LegendStudioUserDataHelper.js +120 -1
  12. package/lib/__lib__/LegendStudioUserDataHelper.js.map +1 -1
  13. package/lib/components/editor/editor-group/data-editor/EmbeddedDataEditor.d.ts +1 -1
  14. package/lib/components/editor/editor-group/data-editor/EmbeddedDataEditor.d.ts.map +1 -1
  15. package/lib/components/editor/editor-group/data-editor/EmbeddedDataEditor.js +3 -3
  16. package/lib/components/editor/editor-group/data-editor/EmbeddedDataEditor.js.map +1 -1
  17. package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.d.ts +3 -0
  18. package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.d.ts.map +1 -1
  19. package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.js +13 -35
  20. package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.js.map +1 -1
  21. package/lib/components/editor/editor-group/dataProduct/DataProductEditor.d.ts.map +1 -1
  22. package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js +20 -7
  23. package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js.map +1 -1
  24. package/lib/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.d.ts.map +1 -1
  25. package/lib/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.js +59 -22
  26. package/lib/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.js.map +1 -1
  27. package/lib/components/editor/editor-group/function-activator/testable/FunctionTestableEditor.d.ts.map +1 -1
  28. package/lib/components/editor/editor-group/function-activator/testable/FunctionTestableEditor.js +113 -75
  29. package/lib/components/editor/editor-group/function-activator/testable/FunctionTestableEditor.js.map +1 -1
  30. package/lib/components/editor/editor-group/testable/TestableSharedComponents.d.ts.map +1 -1
  31. package/lib/components/editor/editor-group/testable/TestableSharedComponents.js +2 -2
  32. package/lib/components/editor/editor-group/testable/TestableSharedComponents.js.map +1 -1
  33. package/lib/components/editor/side-bar/DevMetadataPanel.d.ts.map +1 -1
  34. package/lib/components/editor/side-bar/DevMetadataPanel.js +37 -6
  35. package/lib/components/editor/side-bar/DevMetadataPanel.js.map +1 -1
  36. package/lib/components/workspace-setup/RecentWorkspacesPanel.d.ts +22 -0
  37. package/lib/components/workspace-setup/RecentWorkspacesPanel.d.ts.map +1 -0
  38. package/lib/components/workspace-setup/RecentWorkspacesPanel.js +80 -0
  39. package/lib/components/workspace-setup/RecentWorkspacesPanel.js.map +1 -0
  40. package/lib/components/workspace-setup/WorkspaceSetup.d.ts.map +1 -1
  41. package/lib/components/workspace-setup/WorkspaceSetup.js +61 -6
  42. package/lib/components/workspace-setup/WorkspaceSetup.js.map +1 -1
  43. package/lib/index.css +2 -2
  44. package/lib/index.css.map +1 -1
  45. package/lib/package.json +1 -1
  46. package/lib/stores/editor/EditorStore.d.ts.map +1 -1
  47. package/lib/stores/editor/EditorStore.js +31 -0
  48. package/lib/stores/editor/EditorStore.js.map +1 -1
  49. package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.d.ts +1 -1
  50. package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.d.ts.map +1 -1
  51. package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.js +20 -48
  52. package/lib/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.js.map +1 -1
  53. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.d.ts +9 -14
  54. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.d.ts.map +1 -1
  55. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.js +125 -78
  56. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.js.map +1 -1
  57. package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.d.ts +18 -4
  58. package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.d.ts.map +1 -1
  59. package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.js +216 -53
  60. package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.js.map +1 -1
  61. package/lib/stores/editor/sidebar-state/ProjectOverviewState.d.ts.map +1 -1
  62. package/lib/stores/editor/sidebar-state/ProjectOverviewState.js +11 -0
  63. package/lib/stores/editor/sidebar-state/ProjectOverviewState.js.map +1 -1
  64. package/lib/stores/editor/sidebar-state/WorkspaceReviewState.d.ts.map +1 -1
  65. package/lib/stores/editor/sidebar-state/WorkspaceReviewState.js +11 -0
  66. package/lib/stores/editor/sidebar-state/WorkspaceReviewState.js.map +1 -1
  67. package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.d.ts +9 -0
  68. package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.d.ts.map +1 -1
  69. package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.js +57 -1
  70. package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.js.map +1 -1
  71. package/lib/stores/project-reviewer/ProjectReviewerStore.d.ts.map +1 -1
  72. package/lib/stores/project-reviewer/ProjectReviewerStore.js +12 -0
  73. package/lib/stores/project-reviewer/ProjectReviewerStore.js.map +1 -1
  74. package/lib/stores/workspace-setup/WorkspaceSetupStore.d.ts +17 -0
  75. package/lib/stores/workspace-setup/WorkspaceSetupStore.d.ts.map +1 -1
  76. package/lib/stores/workspace-setup/WorkspaceSetupStore.js +61 -0
  77. package/lib/stores/workspace-setup/WorkspaceSetupStore.js.map +1 -1
  78. package/package.json +16 -16
  79. package/src/__lib__/LegendStudioEvent.ts +3 -0
  80. package/src/__lib__/LegendStudioTelemetryHelper.ts +35 -11
  81. package/src/__lib__/LegendStudioUserDataHelper.ts +204 -1
  82. package/src/components/editor/editor-group/data-editor/EmbeddedDataEditor.tsx +4 -0
  83. package/src/components/editor/editor-group/data-editor/RelationElementsDataEditor.tsx +209 -187
  84. package/src/components/editor/editor-group/dataProduct/DataProductEditor.tsx +26 -7
  85. package/src/components/editor/editor-group/dataProduct/testable/DataProductTestableEditor.tsx +149 -86
  86. package/src/components/editor/editor-group/function-activator/testable/FunctionTestableEditor.tsx +425 -308
  87. package/src/components/editor/editor-group/testable/TestableSharedComponents.tsx +3 -11
  88. package/src/components/editor/side-bar/DevMetadataPanel.tsx +194 -10
  89. package/src/components/workspace-setup/RecentWorkspacesPanel.tsx +161 -0
  90. package/src/components/workspace-setup/WorkspaceSetup.tsx +97 -8
  91. package/src/stores/editor/EditorStore.ts +44 -0
  92. package/src/stores/editor/editor-state/element-editor-state/data/EmbeddedDataState.ts +28 -50
  93. package/src/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.ts +164 -100
  94. package/src/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.ts +307 -72
  95. package/src/stores/editor/sidebar-state/ProjectOverviewState.ts +14 -0
  96. package/src/stores/editor/sidebar-state/WorkspaceReviewState.ts +14 -0
  97. package/src/stores/editor/sidebar-state/dev-metadata/DevMetadataState.ts +84 -1
  98. package/src/stores/project-reviewer/ProjectReviewerStore.ts +15 -0
  99. package/src/stores/workspace-setup/WorkspaceSetupStore.ts +93 -0
  100. package/tsconfig.json +1 -0
@@ -93,6 +93,7 @@ import {
93
93
  DEFAULT_TAB_SIZE,
94
94
  } from '@finos/legend-application';
95
95
  import { LEGEND_STUDIO_APP_EVENT } from '../../__lib__/LegendStudioEvent.js';
96
+ import { LegendStudioUserDataHelper } from '../../__lib__/LegendStudioUserDataHelper.js';
96
97
  import type { EditorMode } from './EditorMode.js';
97
98
  import { StandardEditorMode } from './StandardEditorMode.js';
98
99
  import { WorkspaceUpdateConflictResolutionState } from './sidebar-state/WorkspaceUpdateConflictResolutionState.js';
@@ -691,6 +692,12 @@ export class EditorStore implements CommandRegistrar {
691
692
  }),
692
693
  );
693
694
  if (!this.sdlcState.currentProject) {
695
+ // The project the user navigated to doesn't exist (or isn't accessible).
696
+ // Drop it from the recents cache so we don't keep offering a dead link.
697
+ LegendStudioUserDataHelper.workspaceSetup_removeRecentProject(
698
+ this.applicationStore.userDataService,
699
+ projectId,
700
+ );
694
701
  // If the project is not found or the user does not have access to it,
695
702
  // we will not automatically redirect them to the setup page as they will lose the URL
696
703
  // instead, we give them the option to:
@@ -740,6 +747,15 @@ export class EditorStore implements CommandRegistrar {
740
747
  ),
741
748
  );
742
749
  if (!this.sdlcState.currentWorkspace) {
750
+ // The workspace the user navigated to doesn't exist anymore. Drop
751
+ // the matching entry from the recents cache (no-op for patch
752
+ // workspaces, which are never cached in the first place).
753
+ if (patchReleaseVersionId === undefined) {
754
+ LegendStudioUserDataHelper.workspaceSetup_removeRecentWorkspace(
755
+ this.applicationStore.userDataService,
756
+ { projectId, workspaceId, workspaceType },
757
+ );
758
+ }
743
759
  // If the workspace is not found,
744
760
  // we will not automatically redirect the user to the setup page as they will lose the URL
745
761
  // instead, we give them the option to:
@@ -810,6 +826,34 @@ export class EditorStore implements CommandRegistrar {
810
826
  onLeave(false);
811
827
  return;
812
828
  }
829
+ // At this point both the project and the workspace have been confirmed
830
+ // to exist on the server (the guards above bail out otherwise), so this
831
+ // is the authoritative "the user actually opened this workspace" moment.
832
+ // Record it in the recents cache so the workspace setup screen can offer
833
+ // it instantly next time.
834
+ // NOTE: patch-based workspaces are intentionally excluded from recents
835
+ // (they are not surfaced in the recents UI). Sandbox projects ARE
836
+ // included — opening one is still a meaningful "I worked here" signal,
837
+ // and surfacing it alongside other recents gives a faster one-click
838
+ // re-entry than waiting for the dedicated sandbox loader.
839
+ if (this.sdlcState.currentWorkspace.source === undefined) {
840
+ LegendStudioUserDataHelper.workspaceSetup_recordRecentProject(
841
+ this.applicationStore.userDataService,
842
+ {
843
+ projectId: this.sdlcState.currentProject.projectId,
844
+ name: this.sdlcState.currentProject.name,
845
+ },
846
+ );
847
+ LegendStudioUserDataHelper.workspaceSetup_recordRecentWorkspace(
848
+ this.applicationStore.userDataService,
849
+ {
850
+ projectId: this.sdlcState.currentProject.projectId,
851
+ workspaceId: this.sdlcState.currentWorkspace.workspaceId,
852
+ workspaceType: this.sdlcState.currentWorkspace.workspaceType,
853
+ },
854
+ );
855
+ }
856
+
813
857
  yield Promise.all([
814
858
  this.sdlcState.fetchCurrentRevision(
815
859
  projectId,
@@ -32,6 +32,10 @@ import {
32
32
  } from '@finos/legend-graph';
33
33
  import {
34
34
  ContentType,
35
+ csvDecodeValue,
36
+ csvEncodeValue,
37
+ csvStringify,
38
+ parseCSVContent,
35
39
  guaranteeNonEmptyString,
36
40
  tryToFormatLosslessJSONString,
37
41
  UnsupportedOperationError,
@@ -273,10 +277,16 @@ export class RelationElementState {
273
277
 
274
278
  updateRow(rowIndex: number, columnIndex: number, value: string): void {
275
279
  if (this.relationElement.rows[rowIndex]) {
276
- this.relationElement.rows[rowIndex].values[columnIndex] = value;
280
+ this.relationElement.rows[rowIndex].values[columnIndex] =
281
+ csvEncodeValue(value);
277
282
  }
278
283
  }
279
284
 
285
+ getDisplayValue(rowIndex: number, columnIndex: number): string {
286
+ const value = this.relationElement.rows[rowIndex]?.values[columnIndex];
287
+ return value !== undefined ? csvDecodeValue(value) : '';
288
+ }
289
+
280
290
  clearAllData(): void {
281
291
  this.relationElement.rows.splice(0);
282
292
  }
@@ -285,7 +295,9 @@ export class RelationElementState {
285
295
  return JSON.stringify(
286
296
  {
287
297
  columns: this.relationElement.columns,
288
- data: this.relationElement.rows,
298
+ data: this.relationElement.rows.map((row) => ({
299
+ values: row.values.map((v) => csvDecodeValue(v)),
300
+ })),
289
301
  },
290
302
  null,
291
303
  2,
@@ -310,7 +322,7 @@ export class RelationElementState {
310
322
  const insertStatements = this.relationElement.rows.map((row) => {
311
323
  const values = this.relationElement.columns
312
324
  .map((col, colIndex) => {
313
- const value = row.values[colIndex] ?? '';
325
+ const value = csvDecodeValue(row.values[colIndex] ?? '');
314
326
  if (value !== '') {
315
327
  return `'${value.replace(/'/g, "''")}'`;
316
328
  }
@@ -324,64 +336,30 @@ export class RelationElementState {
324
336
  }
325
337
 
326
338
  exportCSV(): string {
327
- const headers = this.relationElement.columns.map((col) => col);
328
- const csvLines = [headers.join(',')];
329
-
330
- this.relationElement.rows.forEach((row) => {
331
- const values = headers.map((header, headerIndex) => {
332
- const value = row.values[headerIndex] ?? '';
333
- if (value.includes(',') || value.includes('"')) {
334
- return `"${value.replace(/"/g, '""')}"`;
335
- }
336
- return value;
337
- });
338
- csvLines.push(values.join(','));
339
- });
340
-
341
- return csvLines.join('\n');
342
- }
343
-
344
- private parseCSVLine(line: string): string[] {
345
- const result: string[] = [];
346
- let current = '';
347
- let inQuotes = false;
348
-
349
- for (let i = 0; i < line.length; i++) {
350
- const char = line[i];
351
- if (char === '"') {
352
- inQuotes = !inQuotes;
353
- } else if (char === ',' && !inQuotes) {
354
- result.push(current.trim());
355
- current = '';
356
- } else {
357
- current += char;
358
- }
359
- }
360
- result.push(current.trim());
361
- return result;
339
+ // decode so that csvStringify does not double encode
340
+ const data = this.relationElement.rows.map((row) =>
341
+ row.values.map((v) => csvDecodeValue(v)),
342
+ );
343
+ return csvStringify([this.relationElement.columns, ...data]);
362
344
  }
363
345
 
364
346
  importCSV(csvContent: string): void {
365
- const lines = csvContent.trim().split('\n');
366
- if (lines.length === 0) {
347
+ const parsed = parseCSVContent(csvContent);
348
+ if (parsed.length === 0) {
367
349
  return;
368
350
  }
369
351
 
370
- const firstLine = lines[0];
371
- if (!firstLine) {
352
+ const headers = parsed[0];
353
+ if (!headers) {
372
354
  return;
373
355
  }
374
356
 
375
- const headers = this.parseCSVLine(firstLine);
376
357
  this.relationElement.columns = headers;
377
-
378
- this.relationElement.rows = lines.slice(1).map((line) => {
379
- const values = this.parseCSVLine(line);
358
+ this.relationElement.rows = parsed.slice(1).map((values) => {
380
359
  const row = new RelationRowTestData();
381
- row.values = [];
382
- headers.forEach((header, index) => {
383
- row.values[index] = values[index] ?? '';
384
- });
360
+ row.values = headers.map((_, index) =>
361
+ csvEncodeValue(values[index] ?? ''),
362
+ );
385
363
  return observe_RelationRowTestData(row);
386
364
  });
387
365
  }
@@ -26,6 +26,7 @@ import {
26
26
  LakehouseAccessPoint,
27
27
  DataProductTestSuite,
28
28
  BaseDataResolver,
29
+ ReferenceDataResolver,
29
30
  DataProductAccessPointTest,
30
31
  RelationElementsData,
31
32
  RelationElement,
@@ -34,11 +35,14 @@ import {
34
35
  TestError,
35
36
  TestExecutionStatus,
36
37
  EqualToRelation,
38
+ RelationRowTestData,
37
39
  observe_RelationElement,
40
+ observe_RelationRowTestData,
38
41
  observe_RelationElementsData,
39
42
  observe_DataProductTestSuite,
40
43
  IngestDefinition,
41
44
  getAccessorItemLabelForElement,
45
+ type AbstractPureGraphManager,
42
46
  } from '@finos/legend-graph';
43
47
  import {
44
48
  type GeneratorFn,
@@ -59,7 +63,10 @@ import {
59
63
  } from 'mobx';
60
64
  import type { EditorStore } from '../../../../EditorStore.js';
61
65
  import type { DataProductEditorState } from '../DataProductEditorState.js';
62
- import { RelationElementState } from '../../data/EmbeddedDataState.js';
66
+ import {
67
+ RelationElementsDataState,
68
+ RelationElementState,
69
+ } from '../../data/EmbeddedDataState.js';
63
70
  import { TESTABLE_RESULT } from '../../../../sidebar-state/testable/GlobalTestRunnerState.js';
64
71
  import { testSuite_addTest } from '../../../../../graph-modifier/Testable_GraphModifierHelper.js';
65
72
  import {
@@ -71,10 +78,12 @@ const createEmptyRelationElement = (
71
78
  itemId: string,
72
79
  columns: string[] = [],
73
80
  ): RelationElement => {
81
+ const row = observe_RelationRowTestData(new RelationRowTestData());
82
+ row.values = columns.map(() => '');
74
83
  const relationElement = new RelationElement();
75
84
  relationElement.paths = [itemId];
76
85
  relationElement.columns = columns;
77
- relationElement.rows = [];
86
+ relationElement.rows = [row];
78
87
  return observe_RelationElement(relationElement);
79
88
  };
80
89
 
@@ -123,6 +132,7 @@ interface ElementDataItem {
123
132
 
124
133
  const getElementDataItems = (
125
134
  element: PackageableElement,
135
+ graphManager: AbstractPureGraphManager,
126
136
  ): ElementDataItem[] => {
127
137
  if (element instanceof DataProduct) {
128
138
  return element.accessPointGroups
@@ -133,10 +143,12 @@ const getElementDataItems = (
133
143
  }));
134
144
  }
135
145
  if (element instanceof IngestDefinition) {
136
- return (element.TEMPORARY_MATVIEW_FUNCTION_DATA_SETS ?? []).map((ds) => ({
137
- id: ds.name,
138
- label: ds.name,
139
- }));
146
+ return graphManager
147
+ .getIngestDefinitionDatasetNames(element)
148
+ .map((name) => ({
149
+ id: name,
150
+ label: name,
151
+ }));
140
152
  }
141
153
  return [];
142
154
  };
@@ -252,31 +264,21 @@ export class DataProductElementTestDataState {
252
264
  readonly testDataState: DataProductTestDataState;
253
265
  readonly testData: BaseDataResolver;
254
266
  readonly editorStore: EditorStore;
255
-
256
- /** Currently selected dataset/item within this element's RelationElementsData */
257
- selectedItemId: string | undefined;
258
- /** RelationElementState for the currently selected item */
259
- relationElementState: RelationElementState | undefined;
267
+ readonly relationElementsDataState: RelationElementsDataState | undefined;
260
268
 
261
269
  constructor(
262
270
  testDataState: DataProductTestDataState,
263
271
  testData: BaseDataResolver,
264
272
  ) {
265
- makeObservable(this, {
266
- selectedItemId: observable,
267
- relationElementState: observable,
268
- setSelectedItem: action,
269
- });
270
273
  this.testDataState = testDataState;
271
274
  this.testData = testData;
272
275
  this.editorStore = testDataState.editorStore;
273
- // Select the first item that has data
274
276
  if (testData.data instanceof RelationElementsData) {
275
- const firstRel = testData.data.relationElements[0];
276
- if (firstRel) {
277
- this.selectedItemId = firstRel.paths[0];
278
- this.relationElementState = new RelationElementState(firstRel);
279
- }
277
+ this.relationElementsDataState = new RelationElementsDataState(
278
+ this.editorStore,
279
+ testData.data,
280
+ );
281
+ this.initAccessorOptions();
280
282
  }
281
283
  }
282
284
 
@@ -292,35 +294,81 @@ export class DataProductElementTestDataState {
292
294
  return getAccessorItemLabelForElement(this.element as AccessorOwner);
293
295
  }
294
296
 
295
- get configuredItemIds(): string[] {
296
- if (this.testData.data instanceof RelationElementsData) {
297
- return this.testData.data.relationElements.map((re) => re.paths[0] ?? '');
297
+ private initAccessorOptions(): void {
298
+ const dataState = this.relationElementsDataState;
299
+ if (!dataState) {
300
+ return;
298
301
  }
299
- return [];
300
- }
301
-
302
- get configuredItems(): ElementDataItem[] {
303
- const availableItemsById = new Map(
304
- getElementDataItems(this.element).map((item) => [item.id, item.label]),
302
+ this.refreshAccessorOptions(dataState).catch(noop);
303
+ dataState.setRefreshAccessorOptions(() =>
304
+ this.refreshAccessorOptions(dataState),
305
305
  );
306
- return this.configuredItemIds.map((id) => ({
307
- id,
308
- label: availableItemsById.get(id) ?? id,
309
- }));
310
306
  }
311
307
 
312
- setSelectedItem(itemId: string | undefined): void {
313
- this.selectedItemId = itemId;
314
- if (itemId && this.testData.data instanceof RelationElementsData) {
315
- const relEl = this.testData.data.relationElements.find(
316
- (re) => re.paths[0] === itemId,
317
- );
318
- this.relationElementState = relEl
319
- ? new RelationElementState(relEl)
320
- : undefined;
321
- } else {
322
- this.relationElementState = undefined;
308
+ private async refreshAccessorOptions(
309
+ dataState: RelationElementsDataState,
310
+ ): Promise<void> {
311
+ const element = this.element;
312
+ const graphManager = this.editorStore.graphManagerState.graphManager;
313
+ const graph = this.editorStore.graphManagerState.graph;
314
+ const items = getElementDataItems(element, graphManager);
315
+ if (items.length === 0) {
316
+ dataState.setAccessorOptions(undefined, undefined);
317
+ return;
323
318
  }
319
+ const typeLabel = this.itemLabel;
320
+ const options = await Promise.all(
321
+ items.map(async (item) => {
322
+ let columns: string[] = [];
323
+ try {
324
+ if (element instanceof IngestDefinition) {
325
+ const accessor = graphManager.createAccessorFromPackageableElement(
326
+ element,
327
+ graph,
328
+ { schemaName: undefined, tableName: item.id },
329
+ );
330
+ if (accessor) {
331
+ columns = accessor.relationType.columns.map((c) => c.name);
332
+ }
333
+ } else if (element instanceof DataProduct) {
334
+ const accessor = await graphManager.buildDataProductAccessor(
335
+ element,
336
+ graph,
337
+ { tableName: item.id },
338
+ );
339
+ if (accessor) {
340
+ columns = accessor.relationType.columns.map((c) => c.name);
341
+ }
342
+ }
343
+ } catch {
344
+ // best-effort column resolution
345
+ }
346
+ return {
347
+ label: item.label,
348
+ value: item.id,
349
+ columns,
350
+ };
351
+ }),
352
+ );
353
+ runInAction(() => {
354
+ dataState.setAccessorOptions(options, typeLabel);
355
+ // Back-fill columns on existing relation elements that have none
356
+ const columnsByItem = new Map(
357
+ options
358
+ .filter((o) => o.columns.length > 0)
359
+ .map((o) => [o.value, o.columns]),
360
+ );
361
+ for (const relState of dataState.relationElementStates) {
362
+ const rel = relState.relationElement;
363
+ if (rel.columns.length === 0) {
364
+ const key = rel.paths[rel.paths.length - 1];
365
+ const cols = key ? columnsByItem.get(key) : undefined;
366
+ if (cols) {
367
+ rel.columns = cols;
368
+ }
369
+ }
370
+ }
371
+ });
324
372
  }
325
373
  }
326
374
 
@@ -332,6 +380,7 @@ export class DataProductTestDataState {
332
380
 
333
381
  elementTestDataStates: DataProductElementTestDataState[] = [];
334
382
  selectedElementTestDataState: DataProductElementTestDataState | undefined;
383
+ showAddElementModal = false;
335
384
 
336
385
  constructor(
337
386
  suiteState: DataProductTestSuiteState,
@@ -343,7 +392,11 @@ export class DataProductTestDataState {
343
392
  makeObservable(this, {
344
393
  elementTestDataStates: observable,
345
394
  selectedElementTestDataState: observable,
395
+ showAddElementModal: observable,
346
396
  setSelectedElementTestDataState: action,
397
+ setShowAddElementModal: action,
398
+ addElement: action,
399
+ deleteElement: action,
347
400
  refreshElementTestDataStates: action,
348
401
  });
349
402
  this.editorStore = suiteState.editorStore;
@@ -351,16 +404,64 @@ export class DataProductTestDataState {
351
404
  this.refreshElementTestDataStates(options);
352
405
  }
353
406
 
407
+ get availableElementsToAdd(): PackageableElement[] {
408
+ const suite = this.suiteState.suite;
409
+ const existingPaths = new Set(
410
+ (suite.testData ?? [])
411
+ .filter(
412
+ (td): td is BaseDataResolver | ReferenceDataResolver =>
413
+ td instanceof BaseDataResolver ||
414
+ td instanceof ReferenceDataResolver,
415
+ )
416
+ .map((td) => td.element.value.path),
417
+ );
418
+ const graph = this.editorStore.graphManagerState.graph;
419
+ const currentDpPath = this.suiteState.testableState.dataProduct.path;
420
+ const candidates: PackageableElement[] = [
421
+ ...graph.ingests,
422
+ ...graph.allElements.filter(
423
+ (e) => e instanceof DataProduct && e.path !== currentDpPath,
424
+ ),
425
+ ];
426
+ return candidates.filter((e) => !existingPaths.has(e.path));
427
+ }
428
+
429
+ setShowAddElementModal(val: boolean): void {
430
+ this.showAddElementModal = val;
431
+ }
432
+
433
+ addElement(path: string): void {
434
+ const element =
435
+ this.editorStore.graphManagerState.graph.getNullableElement(path);
436
+ if (!element) {
437
+ return;
438
+ }
439
+ const resolver = new BaseDataResolver();
440
+ resolver.element = PackageableElementExplicitReference.create(element);
441
+ const relData = new RelationElementsData();
442
+ relData.relationElements = [];
443
+ observe_RelationElementsData(relData);
444
+ resolver.data = relData;
445
+ const suite = this.suiteState.suite;
446
+ suite.testData = [...(suite.testData ?? []), resolver];
447
+ this.refreshElementTestDataStates({ selectedElementPath: path });
448
+ }
449
+
450
+ deleteElement(elementState: DataProductElementTestDataState): void {
451
+ const suite = this.suiteState.suite;
452
+ if (suite.testData) {
453
+ const idx = suite.testData.indexOf(elementState.testData);
454
+ suite.testData.splice(idx, 1);
455
+ }
456
+ this.refreshElementTestDataStates();
457
+ }
458
+
354
459
  refreshElementTestDataStates(options?: {
355
460
  selectedElementPath?: string | undefined;
356
- selectedItemId?: string | undefined;
357
461
  }): void {
358
462
  const previouslySelectedElementPath =
359
463
  options?.selectedElementPath ??
360
464
  this.selectedElementTestDataState?.element.path;
361
- const previouslySelectedItemId =
362
- options?.selectedItemId ??
363
- this.selectedElementTestDataState?.selectedItemId;
364
465
  const suite = this.suiteState.suite;
365
466
  this.elementTestDataStates = (suite.testData ?? [])
366
467
  .filter((td): td is BaseDataResolver => td instanceof BaseDataResolver)
@@ -370,14 +471,6 @@ export class DataProductTestDataState {
370
471
  this.elementTestDataStates.find(
371
472
  (state) => state.element.path === previouslySelectedElementPath,
372
473
  ) ?? this.elementTestDataStates[0];
373
-
374
- if (this.selectedElementTestDataState && previouslySelectedItemId) {
375
- const nextSelectedItemId =
376
- this.selectedElementTestDataState.configuredItemIds.find(
377
- (itemId) => itemId === previouslySelectedItemId,
378
- ) ?? this.selectedElementTestDataState.configuredItemIds[0];
379
- this.selectedElementTestDataState.setSelectedItem(nextSelectedItemId);
380
- }
381
474
  }
382
475
 
383
476
  setSelectedElementTestDataState(
@@ -427,8 +520,6 @@ export class DataProductTestSuiteState extends TestableTestSuiteEditorState {
427
520
  this.testDataState = new DataProductTestDataState(this, {
428
521
  selectedElementPath:
429
522
  this.testDataState.selectedElementTestDataState?.element.path,
430
- selectedItemId:
431
- this.testDataState.selectedElementTestDataState?.selectedItemId,
432
523
  });
433
524
  }
434
525
 
@@ -540,42 +631,19 @@ export class DataProductTestSuiteState extends TestableTestSuiteEditorState {
540
631
  }
541
632
  }
542
633
  } else {
543
- // Fallback: single resolver on current DP
544
- let relationData = this.suite.testData?.find(
545
- (td): td is BaseDataResolver =>
546
- td instanceof BaseDataResolver &&
547
- td.element.value === this.testableState.dataProduct &&
548
- td.data instanceof RelationElementsData,
549
- )?.data as RelationElementsData | undefined;
550
-
551
- if (!relationData) {
552
- const testData = new BaseDataResolver();
553
- testData.element = PackageableElementExplicitReference.create(
554
- this.testableState.dataProduct,
555
- );
556
- relationData = new RelationElementsData();
557
- relationData.relationElements = [];
558
- observe_RelationElementsData(relationData);
559
- testData.data = relationData;
560
- this.suite.testData = [...(this.suite.testData ?? []), testData];
561
- }
562
- if (
563
- !relationData.relationElements.find(
564
- (re) => re.paths[0] === accessPointId,
565
- )
566
- ) {
567
- relationData.relationElements.push(
568
- createEmptyRelationElement(accessPointId, inferredColumns),
569
- );
570
- }
634
+ this.editorStore.applicationStore.notificationService.notifyWarning(
635
+ 'Access Point accessors cannot be resolved',
636
+ );
571
637
  }
572
638
 
573
639
  const assertion = new EqualToRelation();
574
640
  assertion.id = 'assert_1';
575
641
  const expectedRelElement = new RelationElement();
642
+ const expectedRow = observe_RelationRowTestData(new RelationRowTestData());
643
+ expectedRow.values = inferredColumns.map(() => '');
576
644
  expectedRelElement.paths = [accessPointId];
577
645
  expectedRelElement.columns = inferredColumns;
578
- expectedRelElement.rows = [];
646
+ expectedRelElement.rows = [expectedRow];
579
647
  observe_RelationElement(expectedRelElement);
580
648
  assertion.expected = expectedRelElement;
581
649
  test.assertions = [assertion];
@@ -792,17 +860,11 @@ export class DataProductTestableState {
792
860
  suite.testData.push(resolver);
793
861
  }
794
862
  }
795
- // Fallback: no external sources resolved seed a single resolver on current DP
863
+ // If no external sources were resolved, notify the user and leave test data empty
796
864
  if (suite.testData.length === 0) {
797
- const testData = new BaseDataResolver();
798
- testData.element = PackageableElementExplicitReference.create(dp);
799
- const relData = new RelationElementsData();
800
- relData.relationElements = [
801
- createEmptyRelationElement(accessPointId, inferredColumns),
802
- ];
803
- observe_RelationElementsData(relData);
804
- testData.data = relData;
805
- suite.testData = [testData];
865
+ this.editorStore.applicationStore.notificationService.notifyWarning(
866
+ 'Access Point accessors cannot be resolved',
867
+ );
806
868
  }
807
869
 
808
870
  // Create one initial test with EqualToRelation assertion
@@ -814,9 +876,11 @@ export class DataProductTestableState {
814
876
  const assertion = new EqualToRelation();
815
877
  assertion.id = 'assert_1';
816
878
  const expectedRelElement = new RelationElement();
879
+ const expectedRow = observe_RelationRowTestData(new RelationRowTestData());
880
+ expectedRow.values = inferredColumns.map(() => '');
817
881
  expectedRelElement.paths = [accessPointId];
818
882
  expectedRelElement.columns = inferredColumns;
819
- expectedRelElement.rows = [];
883
+ expectedRelElement.rows = [expectedRow];
820
884
  observe_RelationElement(expectedRelElement);
821
885
  assertion.expected = expectedRelElement;
822
886
  test.assertions = [assertion];