@finos/legend-application-studio 28.21.6 → 28.21.8

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 (37) hide show
  1. package/lib/__lib__/LegendStudioUserDataHelper.d.ts +23 -1
  2. package/lib/__lib__/LegendStudioUserDataHelper.d.ts.map +1 -1
  3. package/lib/__lib__/LegendStudioUserDataHelper.js +66 -1
  4. package/lib/__lib__/LegendStudioUserDataHelper.js.map +1 -1
  5. package/lib/components/editor/editor-group/accessor/AccessorQueryBuilderHelper.js +1 -1
  6. package/lib/components/editor/editor-group/accessor/AccessorQueryBuilderHelper.js.map +1 -1
  7. package/lib/components/editor/editor-group/dataProduct/DataProductEditor.d.ts.map +1 -1
  8. package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js +7 -20
  9. package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js.map +1 -1
  10. package/lib/components/workspace-setup/RecentWorkspacesPanel.d.ts.map +1 -1
  11. package/lib/components/workspace-setup/RecentWorkspacesPanel.js +11 -5
  12. package/lib/components/workspace-setup/RecentWorkspacesPanel.js.map +1 -1
  13. package/lib/components/workspace-setup/WorkspaceSetup.d.ts.map +1 -1
  14. package/lib/components/workspace-setup/WorkspaceSetup.js +5 -6
  15. package/lib/components/workspace-setup/WorkspaceSetup.js.map +1 -1
  16. package/lib/index.css +2 -2
  17. package/lib/index.css.map +1 -1
  18. package/lib/package.json +1 -1
  19. package/lib/stores/editor/EditorStore.d.ts.map +1 -1
  20. package/lib/stores/editor/EditorStore.js +3 -0
  21. package/lib/stores/editor/EditorStore.js.map +1 -1
  22. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.d.ts.map +1 -1
  23. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.js +1 -1
  24. package/lib/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.js.map +1 -1
  25. package/lib/stores/workspace-setup/WorkspaceSetupStore.d.ts +6 -2
  26. package/lib/stores/workspace-setup/WorkspaceSetupStore.d.ts.map +1 -1
  27. package/lib/stores/workspace-setup/WorkspaceSetupStore.js +60 -8
  28. package/lib/stores/workspace-setup/WorkspaceSetupStore.js.map +1 -1
  29. package/package.json +9 -9
  30. package/src/__lib__/LegendStudioUserDataHelper.ts +107 -2
  31. package/src/components/editor/editor-group/accessor/AccessorQueryBuilderHelper.tsx +1 -1
  32. package/src/components/editor/editor-group/dataProduct/DataProductEditor.tsx +8 -26
  33. package/src/components/workspace-setup/RecentWorkspacesPanel.tsx +29 -9
  34. package/src/components/workspace-setup/WorkspaceSetup.tsx +5 -6
  35. package/src/stores/editor/EditorStore.ts +3 -0
  36. package/src/stores/editor/editor-state/element-editor-state/dataProduct/testable/DataProductTestableState.ts +6 -5
  37. package/src/stores/workspace-setup/WorkspaceSetupStore.ts +79 -9
@@ -847,7 +847,7 @@ export const LakehouseDataProductAccessPointEditor = observer(
847
847
  applicationStore.config.options.queryBuilderConfig,
848
848
  editorStore.editorMode.getSourceInfo(),
849
849
  );
850
- queryBuilderState.changeAccessorOwner(ingestDefinition);
850
+ await queryBuilderState.changeAccessorOwner(ingestDefinition);
851
851
  const compatibleRuntimes = getCompatibleRuntimesFromAccessorOwner(
852
852
  ingestDefinition,
853
853
  editorStore.graphManagerState,
@@ -2232,9 +2232,6 @@ const AccessPointGroupTab = observer(
2232
2232
  const DataProductSidebar = observer(
2233
2233
  (props: { dataProductEditorState: DataProductEditorState }) => {
2234
2234
  const { dataProductEditorState } = props;
2235
- const showTestingTab =
2236
- dataProductEditorState.editorStore.applicationStore.config.options
2237
- .NonProductionFeatureFlag;
2238
2235
  const sidebarTabs = [
2239
2236
  {
2240
2237
  label: DATA_PRODUCT_TAB.HOME,
@@ -2250,15 +2247,11 @@ const DataProductSidebar = observer(
2250
2247
  title: 'Operational Metadata',
2251
2248
  icon: <GearSuggestIcon />,
2252
2249
  },
2253
- ...(showTestingTab
2254
- ? [
2255
- {
2256
- label: DATA_PRODUCT_TAB.TESTING,
2257
- title: 'Testing',
2258
- icon: <FlaskIcon />,
2259
- },
2260
- ]
2261
- : []),
2250
+ {
2251
+ label: DATA_PRODUCT_TAB.TESTING,
2252
+ title: 'Testing',
2253
+ icon: <FlaskIcon />,
2254
+ },
2262
2255
  {
2263
2256
  label: DATA_PRODUCT_TAB.SUPPORT,
2264
2257
  icon: <QuestionCircleIcon />,
@@ -3367,8 +3360,6 @@ export const DataProductEditor = observer(() => {
3367
3360
  const [showPreview, setShowPreview] = useState(false);
3368
3361
  const [dataProductViewerState, setDataProductViewerState] =
3369
3362
  useState<DataProductViewerState>();
3370
- const showTestingTab =
3371
- editorStore.applicationStore.config.options.NonProductionFeatureFlag;
3372
3363
 
3373
3364
  const selectedActivity = dataProductEditorState.selectedTab;
3374
3365
  const renderActivivtyBarTab = (): React.ReactNode => {
@@ -3402,12 +3393,12 @@ export const DataProductEditor = observer(() => {
3402
3393
  />
3403
3394
  );
3404
3395
  case DATA_PRODUCT_TAB.TESTING:
3405
- return showTestingTab ? (
3396
+ return (
3406
3397
  <DataProductTestableEditor
3407
3398
  dataProductEditorState={dataProductEditorState}
3408
3399
  isReadOnly={isReadOnly}
3409
3400
  />
3410
- ) : null;
3401
+ );
3411
3402
  default:
3412
3403
  return null;
3413
3404
  }
@@ -3423,15 +3414,6 @@ export const DataProductEditor = observer(() => {
3423
3414
  );
3424
3415
  }, [dataProductEditorState]);
3425
3416
 
3426
- useEffect(() => {
3427
- if (
3428
- !showTestingTab &&
3429
- dataProductEditorState.selectedTab === DATA_PRODUCT_TAB.TESTING
3430
- ) {
3431
- dataProductEditorState.setSelectedTab(DATA_PRODUCT_TAB.HOME);
3432
- }
3433
- }, [dataProductEditorState, showTestingTab]);
3434
-
3435
3417
  useEffect(
3436
3418
  () =>
3437
3419
  autorun(
@@ -65,15 +65,15 @@ export const RecentWorkspacesPanel = observer(
65
65
  const applicationStore = setupStore.applicationStore;
66
66
 
67
67
  // Recents are stored in LRU order (most-recent first). Show the top N
68
- // workspaces; map each to its project name (or fall back to projectId
69
- // if the matching project entry was evicted independently).
68
+ // workspaces; look up each one's cached project entry for richer tile
69
+ // metadata (name, description tooltip, tag badges).
70
70
  const tiles = setupStore.recentWorkspaces.slice(0, MAX_TILES);
71
71
  if (tiles.length === 0) {
72
72
  return null;
73
73
  }
74
74
 
75
- const projectNameById = new Map(
76
- setupStore.recentProjects.map((p) => [p.projectId, p.name]),
75
+ const projectById = new Map(
76
+ setupStore.recentProjects.map((p) => [p.projectId, p]),
77
77
  );
78
78
 
79
79
  const openWorkspace = (
@@ -96,8 +96,14 @@ export const RecentWorkspacesPanel = observer(
96
96
  </div>
97
97
  <div className="workspace-setup__recents__grid">
98
98
  {tiles.map((entry) => {
99
- const projectName =
100
- projectNameById.get(entry.projectId) ?? entry.projectId;
99
+ const cachedProject = projectById.get(entry.projectId);
100
+ const projectName = cachedProject?.name ?? entry.projectId;
101
+ const projectDescription = cachedProject?.description ?? '';
102
+ const projectTags = cachedProject?.tags ?? [];
103
+ const tileTitle =
104
+ projectDescription.trim().length > 0
105
+ ? `${projectName} / ${entry.workspaceId}\n${projectDescription}`
106
+ : `Open ${projectName} / ${entry.workspaceId}`;
101
107
  const key = `${entry.projectId}::${entry.workspaceType}::${entry.workspaceId}`;
102
108
  const handleRemove = (
103
109
  event: React.MouseEvent<HTMLButtonElement>,
@@ -114,7 +120,7 @@ export const RecentWorkspacesPanel = observer(
114
120
  key={key}
115
121
  type="button"
116
122
  className="workspace-setup__recents__tile"
117
- title={`Open ${projectName} / ${entry.workspaceId}`}
123
+ title={tileTitle}
118
124
  onClick={() =>
119
125
  openWorkspace(
120
126
  entry.projectId,
@@ -148,8 +154,22 @@ export const RecentWorkspacesPanel = observer(
148
154
  {entry.workspaceId}
149
155
  </span>
150
156
  </div>
151
- <div className="workspace-setup__recents__tile__time">
152
- {formatRelativeTime(entry.lastOpenedAt)}
157
+ <div className="workspace-setup__recents__tile__meta">
158
+ {projectTags.length > 0 && (
159
+ <div className="workspace-setup__recents__tile__tags">
160
+ {projectTags.slice(0, 2).map((tag) => (
161
+ <span
162
+ key={tag}
163
+ className="workspace-setup__recents__tile__tag"
164
+ >
165
+ {tag}
166
+ </span>
167
+ ))}
168
+ </div>
169
+ )}
170
+ <div className="workspace-setup__recents__tile__time">
171
+ {formatRelativeTime(entry.lastOpenedAt)}
172
+ </div>
153
173
  </div>
154
174
  </button>
155
175
  );
@@ -378,15 +378,14 @@ export const WorkspaceSetup = withWorkspaceSetupStore(
378
378
  const recentProjectOptions: ProjectOption[] = setupStore.recentProjects
379
379
  .filter((r) => !loadedProjectIds.has(r.projectId))
380
380
  .map((r) => {
381
- // Construct a lightweight Project stand-in so the existing selector
382
- // contract is preserved. The full project will be fetched on click
383
- // via `selectRecentProject`.
381
+ // Rebuild a real Project from the cached metadata; no synthetic
382
+ // fields needed since we persist everything the schema requires.
384
383
  const stub = Project.serialization.fromJson({
385
384
  projectId: r.projectId,
386
385
  name: r.name,
387
- description: '',
388
- webUrl: '',
389
- tags: [],
386
+ description: r.description,
387
+ webUrl: r.webUrl,
388
+ tags: r.tags,
390
389
  } as PlainObject<Project>);
391
390
  return { label: stub.name, value: stub };
392
391
  });
@@ -842,6 +842,9 @@ export class EditorStore implements CommandRegistrar {
842
842
  {
843
843
  projectId: this.sdlcState.currentProject.projectId,
844
844
  name: this.sdlcState.currentProject.name,
845
+ description: this.sdlcState.currentProject.description,
846
+ webUrl: this.sdlcState.currentProject.webUrl,
847
+ tags: this.sdlcState.currentProject.tags,
845
848
  },
846
849
  );
847
850
  LegendStudioUserDataHelper.workspaceSetup_recordRecentWorkspace(
@@ -322,11 +322,12 @@ export class DataProductElementTestDataState {
322
322
  let columns: string[] = [];
323
323
  try {
324
324
  if (element instanceof IngestDefinition) {
325
- const accessor = graphManager.createAccessorFromPackageableElement(
326
- element,
327
- graph,
328
- { schemaName: undefined, tableName: item.id },
329
- );
325
+ const accessor =
326
+ await graphManager.createAccessorFromPackageableElement(
327
+ element,
328
+ graph,
329
+ { schemaName: undefined, tableName: item.id },
330
+ );
330
331
  if (accessor) {
331
332
  columns = accessor.relationType.columns.map((c) => c.name);
332
333
  }
@@ -93,7 +93,6 @@ export class WorkspaceSetupStore {
93
93
  loadWorkspacesState = ActionState.create();
94
94
  createWorkspaceState = ActionState.create();
95
95
  showCreateWorkspaceModal = false;
96
- showAdvancedWorkspaceFilterOptions = false;
97
96
 
98
97
  graphManagerState: GraphManagerState;
99
98
 
@@ -115,7 +114,6 @@ export class WorkspaceSetupStore {
115
114
  showCreateProjectModal: observable,
116
115
  workspaces: observable,
117
116
  currentWorkspace: observable,
118
- showAdvancedWorkspaceFilterOptions: observable,
119
117
  loadSandboxState: observable,
120
118
  showCreateWorkspaceModal: observable,
121
119
  sandboxProject: observable,
@@ -128,7 +126,6 @@ export class WorkspaceSetupStore {
128
126
  recentWorkspaces: observable,
129
127
  setShowCreateProjectModal: action,
130
128
  setShowCreateWorkspaceModal: action,
131
- setShowAdvancedWorkspaceFilterOptions: action,
132
129
  setImportProjectSuccessReport: action,
133
130
  setSandboxModal: action,
134
131
  changeWorkspace: action,
@@ -183,10 +180,6 @@ export class WorkspaceSetupStore {
183
180
  this.showCreateWorkspaceModal = val;
184
181
  }
185
182
 
186
- setShowAdvancedWorkspaceFilterOptions(val: boolean): void {
187
- this.showAdvancedWorkspaceFilterOptions = val;
188
- }
189
-
190
183
  setImportProjectSuccessReport(
191
184
  importProjectSuccessReport: ImportProjectSuccessReport | undefined,
192
185
  ): void {
@@ -262,6 +255,12 @@ export class WorkspaceSetupStore {
262
255
  * Fetches a project by id (used when the user picks a cached "recent"
263
256
  * project that may not be in the current search results) and switches to
264
257
  * it. If the project no longer exists, the entry is pruned from recents.
258
+ *
259
+ * NOTE: we deliberately don't short-circuit using the cached recent entry
260
+ * here. Going through `getProject` keeps the prune-on-404 path intact, and
261
+ * the cached metadata is already used elsewhere to make the UI feel fast
262
+ * (dropdown stubs in `WorkspaceSetup.tsx` and tile labels in
263
+ * `RecentWorkspacesPanel.tsx`).
265
264
  */
266
265
  *selectRecentProject(projectId: string): GeneratorFn<void> {
267
266
  this.selectRecentProjectState.inProgress();
@@ -304,6 +303,12 @@ export class WorkspaceSetupStore {
304
303
  message: `Sandbox project ${sandboxProject.projectId} created. Creating default workspace...`,
305
304
  showLoading: true,
306
305
  });
306
+ // Invalidate the cached sandbox info so loadSandboxProject re-fetches
307
+ // and persists the newly-created project id instead of reusing the
308
+ // stale "no sandbox yet" cache entry.
309
+ LegendStudioUserDataHelper.workspaceSetup_clearSandboxInfo(
310
+ this.applicationStore.userDataService,
311
+ );
307
312
  yield flowResult(this.loadSandboxProject());
308
313
  const sandbox = guaranteeType(
309
314
  this.sandboxProject,
@@ -460,10 +465,58 @@ export class WorkspaceSetupStore {
460
465
  if (this.enginePromise) {
461
466
  yield this.enginePromise;
462
467
  }
468
+
469
+ const userId = this.sdlcServerClient.currentUser?.userId;
470
+
471
+ // Fast path — if we have a recent, user-matching cache entry, use it to
472
+ // avoid the `userHasPrototypeProjectAccess` graph manager call and the
473
+ // sandbox-tag project search. We still hit SDLC once to confirm the
474
+ // cached projectId is alive, but `getProject(id)` is cheaper than the
475
+ // tagged search and self-invalidates on 404.
476
+ if (userId) {
477
+ const cached =
478
+ LegendStudioUserDataHelper.workspaceSetup_getCachedSandboxInfo(
479
+ this.applicationStore.userDataService,
480
+ userId,
481
+ );
482
+ if (cached) {
483
+ this.hasSandboxAccess = cached.hasAccess;
484
+ if (!cached.hasAccess) {
485
+ // No access, no project — nothing else to do.
486
+ this.sandboxProject = true;
487
+ this.loadSandboxState.pass();
488
+ return;
489
+ }
490
+ if (cached.projectId) {
491
+ try {
492
+ this.sandboxProject = Project.serialization.fromJson(
493
+ (yield this.sdlcServerClient.getProject(
494
+ cached.projectId,
495
+ )) as PlainObject<Project>,
496
+ );
497
+ this.loadSandboxState.pass();
498
+ return;
499
+ } catch {
500
+ // Cached sandbox project no longer exists on the server; drop
501
+ // the cache and fall through to the full refresh.
502
+ LegendStudioUserDataHelper.workspaceSetup_clearSandboxInfo(
503
+ this.applicationStore.userDataService,
504
+ );
505
+ }
506
+ } else {
507
+ // User has access but hasn't created a sandbox yet.
508
+ this.sandboxProject = true;
509
+ this.loadSandboxState.pass();
510
+ return;
511
+ }
512
+ }
513
+ }
514
+
515
+ // Slow path — original flow.
463
516
  const sandboxProject = (
464
517
  (yield this.sdlcServerClient.getProjects(
465
518
  undefined,
466
- this.sdlcServerClient.currentUser?.userId,
519
+ userId,
467
520
  [SANDBOX_SDLC_TAG],
468
521
  1,
469
522
  )) as PlainObject<Project>[]
@@ -471,7 +524,7 @@ export class WorkspaceSetupStore {
471
524
  if (this.hasSandboxAccess === undefined) {
472
525
  this.hasSandboxAccess =
473
526
  (yield this.graphManagerState.graphManager.userHasPrototypeProjectAccess(
474
- this.sdlcServerClient.currentUser?.userId ?? '',
527
+ userId ?? '',
475
528
  )) as boolean;
476
529
  }
477
530
  this.sandboxProject = true;
@@ -482,6 +535,23 @@ export class WorkspaceSetupStore {
482
535
  } else if (sandboxProject.length === 1) {
483
536
  this.sandboxProject = guaranteeNonNullable(sandboxProject[0]);
484
537
  }
538
+
539
+ // Persist the fresh result for next time. We only cache when we have
540
+ // a userId (cache is scoped per user); anonymous sessions skip this.
541
+ if (userId) {
542
+ LegendStudioUserDataHelper.workspaceSetup_recordSandboxInfo(
543
+ this.applicationStore.userDataService,
544
+ {
545
+ userId,
546
+ hasAccess: this.hasSandboxAccess,
547
+ projectId:
548
+ this.sandboxProject instanceof Project
549
+ ? this.sandboxProject.projectId
550
+ : undefined,
551
+ },
552
+ );
553
+ }
554
+
485
555
  this.loadSandboxState.pass();
486
556
  } catch (error) {
487
557
  this.sandboxProject = true;