@finos/legend-application-studio 28.21.5 → 28.21.7
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__/LegendStudioEvent.d.ts +4 -1
- package/lib/__lib__/LegendStudioEvent.d.ts.map +1 -1
- package/lib/__lib__/LegendStudioEvent.js +3 -0
- package/lib/__lib__/LegendStudioEvent.js.map +1 -1
- package/lib/__lib__/LegendStudioTelemetryHelper.d.ts +2 -1
- package/lib/__lib__/LegendStudioTelemetryHelper.d.ts.map +1 -1
- package/lib/__lib__/LegendStudioTelemetryHelper.js +11 -3
- package/lib/__lib__/LegendStudioTelemetryHelper.js.map +1 -1
- package/lib/__lib__/LegendStudioUserDataHelper.d.ts +63 -1
- package/lib/__lib__/LegendStudioUserDataHelper.d.ts.map +1 -1
- package/lib/__lib__/LegendStudioUserDataHelper.js +185 -1
- package/lib/__lib__/LegendStudioUserDataHelper.js.map +1 -1
- package/lib/components/editor/editor-group/data-editor/EmbeddedDataEditor.d.ts.map +1 -1
- package/lib/components/editor/editor-group/data-editor/EmbeddedDataEditor.js +1 -1
- package/lib/components/editor/editor-group/data-editor/EmbeddedDataEditor.js.map +1 -1
- package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.d.ts.map +1 -1
- package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.js +2 -1
- package/lib/components/editor/editor-group/data-editor/RelationElementsDataEditor.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 +1 -1
- package/lib/components/editor/editor-group/dataProduct/DataProductEditor.js.map +1 -1
- package/lib/components/editor/editor-group/testable/TestableSharedComponents.d.ts.map +1 -1
- package/lib/components/editor/editor-group/testable/TestableSharedComponents.js +1 -1
- package/lib/components/editor/editor-group/testable/TestableSharedComponents.js.map +1 -1
- package/lib/components/workspace-setup/RecentWorkspacesPanel.d.ts +22 -0
- package/lib/components/workspace-setup/RecentWorkspacesPanel.d.ts.map +1 -0
- package/lib/components/workspace-setup/RecentWorkspacesPanel.js +86 -0
- package/lib/components/workspace-setup/RecentWorkspacesPanel.js.map +1 -0
- package/lib/components/workspace-setup/WorkspaceSetup.d.ts.map +1 -1
- package/lib/components/workspace-setup/WorkspaceSetup.js +60 -6
- package/lib/components/workspace-setup/WorkspaceSetup.js.map +1 -1
- package/lib/index.css +2 -2
- package/lib/index.css.map +1 -1
- package/lib/package.json +1 -1
- package/lib/stores/editor/EditorStore.d.ts.map +1 -1
- package/lib/stores/editor/EditorStore.js +34 -0
- package/lib/stores/editor/EditorStore.js.map +1 -1
- package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.d.ts.map +1 -1
- package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.js +4 -2
- package/lib/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.js.map +1 -1
- package/lib/stores/editor/sidebar-state/ProjectOverviewState.d.ts.map +1 -1
- package/lib/stores/editor/sidebar-state/ProjectOverviewState.js +11 -0
- package/lib/stores/editor/sidebar-state/ProjectOverviewState.js.map +1 -1
- package/lib/stores/editor/sidebar-state/WorkspaceReviewState.d.ts.map +1 -1
- package/lib/stores/editor/sidebar-state/WorkspaceReviewState.js +11 -0
- package/lib/stores/editor/sidebar-state/WorkspaceReviewState.js.map +1 -1
- package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.d.ts.map +1 -1
- package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.js +2 -1
- package/lib/stores/editor/sidebar-state/dev-metadata/DevMetadataState.js.map +1 -1
- package/lib/stores/project-reviewer/ProjectReviewerStore.d.ts.map +1 -1
- package/lib/stores/project-reviewer/ProjectReviewerStore.js +12 -0
- package/lib/stores/project-reviewer/ProjectReviewerStore.js.map +1 -1
- package/lib/stores/workspace-setup/WorkspaceSetupStore.d.ts +23 -2
- package/lib/stores/workspace-setup/WorkspaceSetupStore.d.ts.map +1 -1
- package/lib/stores/workspace-setup/WorkspaceSetupStore.js +121 -8
- package/lib/stores/workspace-setup/WorkspaceSetupStore.js.map +1 -1
- package/package.json +10 -10
- package/src/__lib__/LegendStudioEvent.ts +3 -0
- package/src/__lib__/LegendStudioTelemetryHelper.ts +35 -11
- package/src/__lib__/LegendStudioUserDataHelper.ts +309 -1
- package/src/components/editor/editor-group/data-editor/EmbeddedDataEditor.tsx +1 -0
- package/src/components/editor/editor-group/data-editor/RelationElementsDataEditor.tsx +13 -5
- package/src/components/editor/editor-group/dataProduct/DataProductEditor.tsx +1 -0
- package/src/components/editor/editor-group/testable/TestableSharedComponents.tsx +1 -0
- package/src/components/workspace-setup/RecentWorkspacesPanel.tsx +181 -0
- package/src/components/workspace-setup/WorkspaceSetup.tsx +96 -8
- package/src/stores/editor/EditorStore.ts +47 -0
- package/src/stores/editor/editor-state/element-editor-state/function-activator/testable/FunctionTestableState.ts +5 -1
- package/src/stores/editor/sidebar-state/ProjectOverviewState.ts +14 -0
- package/src/stores/editor/sidebar-state/WorkspaceReviewState.ts +14 -0
- package/src/stores/editor/sidebar-state/dev-metadata/DevMetadataState.ts +8 -1
- package/src/stores/project-reviewer/ProjectReviewerStore.ts +15 -0
- package/src/stores/workspace-setup/WorkspaceSetupStore.ts +172 -9
- package/tsconfig.json +1 -0
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { UserDataService } from '@finos/legend-application';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type PlainObject,
|
|
4
|
+
SerializationFactory,
|
|
5
|
+
returnUndefOnError,
|
|
6
|
+
usingModelSchema,
|
|
7
|
+
} from '@finos/legend-shared';
|
|
8
|
+
import { WorkspaceType } from '@finos/legend-server-sdlc';
|
|
9
|
+
import { createModelSchema, list, optional, primitive } from 'serializr';
|
|
3
10
|
|
|
4
11
|
/**
|
|
5
12
|
* Copyright (c) 2020-present, Goldman Sachs
|
|
@@ -29,6 +36,139 @@ export enum LEGEND_STUDIO_USER_DATA_KEY {
|
|
|
29
36
|
// drop the toggle button in the tab header, and retarget the SCSS
|
|
30
37
|
// `.database-editor--light` block at the framework's color-theme tokens.
|
|
31
38
|
DATABASE_EDITOR_THEME = 'studio-editor.database-editor.theme',
|
|
39
|
+
// Recently-opened projects and (non-patch) workspaces shown on the
|
|
40
|
+
// workspace setup screen to speed up re-opening common work.
|
|
41
|
+
WORKSPACE_SETUP_RECENTS = 'studio-editor.workspace-setup.recents',
|
|
42
|
+
// Per-user cache of the sandbox-access boolean + the sandbox project id,
|
|
43
|
+
// so the workspace setup screen can render the sandbox UI without waiting
|
|
44
|
+
// on the `userHasPrototypeProjectAccess` graph manager call AND a
|
|
45
|
+
// sandbox-tag project search on every mount. Revalidated against SDLC in
|
|
46
|
+
// the background; invalidated automatically on 404 or after the TTL.
|
|
47
|
+
WORKSPACE_SETUP_SANDBOX_INFO = 'studio-editor.workspace-setup.sandboxInfo',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// --- Workspace setup recents -------------------------------------------------
|
|
51
|
+
|
|
52
|
+
const WORKSPACE_SETUP_RECENTS_VERSION = 1;
|
|
53
|
+
const MAX_RECENT_PROJECTS = 10;
|
|
54
|
+
const MAX_RECENT_WORKSPACES = 20;
|
|
55
|
+
|
|
56
|
+
export class RecentProjectEntry {
|
|
57
|
+
projectId!: string;
|
|
58
|
+
name!: string;
|
|
59
|
+
description!: string;
|
|
60
|
+
webUrl!: string;
|
|
61
|
+
tags!: string[];
|
|
62
|
+
lastOpenedAt!: number;
|
|
63
|
+
|
|
64
|
+
static readonly serialization = new SerializationFactory(
|
|
65
|
+
createModelSchema(RecentProjectEntry, {
|
|
66
|
+
description: primitive(),
|
|
67
|
+
lastOpenedAt: primitive(),
|
|
68
|
+
name: primitive(),
|
|
69
|
+
projectId: primitive(),
|
|
70
|
+
tags: list(primitive()),
|
|
71
|
+
webUrl: primitive(),
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class RecentWorkspaceEntry {
|
|
77
|
+
projectId!: string;
|
|
78
|
+
workspaceId!: string;
|
|
79
|
+
workspaceType!: WorkspaceType;
|
|
80
|
+
lastOpenedAt!: number;
|
|
81
|
+
|
|
82
|
+
static readonly serialization = new SerializationFactory(
|
|
83
|
+
createModelSchema(RecentWorkspaceEntry, {
|
|
84
|
+
lastOpenedAt: primitive(),
|
|
85
|
+
projectId: primitive(),
|
|
86
|
+
workspaceId: primitive(),
|
|
87
|
+
// WorkspaceType is a string enum; stored/loaded as a plain string and
|
|
88
|
+
// validated when the entry is consumed.
|
|
89
|
+
workspaceType: primitive(),
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class WorkspaceSetupRecents {
|
|
95
|
+
version: number = WORKSPACE_SETUP_RECENTS_VERSION;
|
|
96
|
+
projects: RecentProjectEntry[] = [];
|
|
97
|
+
workspaces: RecentWorkspaceEntry[] = [];
|
|
98
|
+
|
|
99
|
+
static readonly serialization = new SerializationFactory(
|
|
100
|
+
createModelSchema(WorkspaceSetupRecents, {
|
|
101
|
+
projects: list(usingModelSchema(RecentProjectEntry.serialization.schema)),
|
|
102
|
+
version: primitive(),
|
|
103
|
+
workspaces: list(
|
|
104
|
+
usingModelSchema(RecentWorkspaceEntry.serialization.schema),
|
|
105
|
+
),
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const isValidWorkspaceType = (v: unknown): v is WorkspaceType =>
|
|
111
|
+
v === WorkspaceType.USER || v === WorkspaceType.GROUP;
|
|
112
|
+
|
|
113
|
+
const emptyRecents = (): WorkspaceSetupRecents => new WorkspaceSetupRecents();
|
|
114
|
+
|
|
115
|
+
const readRecents = (service: UserDataService): WorkspaceSetupRecents => {
|
|
116
|
+
const raw = returnUndefOnError(() =>
|
|
117
|
+
service.getObjectValue(LEGEND_STUDIO_USER_DATA_KEY.WORKSPACE_SETUP_RECENTS),
|
|
118
|
+
);
|
|
119
|
+
if (!raw) {
|
|
120
|
+
return emptyRecents();
|
|
121
|
+
}
|
|
122
|
+
const parsed = returnUndefOnError(() =>
|
|
123
|
+
WorkspaceSetupRecents.serialization.fromJson(
|
|
124
|
+
raw as PlainObject<WorkspaceSetupRecents>,
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
if (!parsed) {
|
|
128
|
+
return emptyRecents();
|
|
129
|
+
}
|
|
130
|
+
// Defensive post-checks: drop entries with invalid enum values and enforce
|
|
131
|
+
// the LRU caps in case the persisted blob was tampered with.
|
|
132
|
+
parsed.workspaces = parsed.workspaces
|
|
133
|
+
.filter((w) => isValidWorkspaceType(w.workspaceType))
|
|
134
|
+
.slice(0, MAX_RECENT_WORKSPACES);
|
|
135
|
+
parsed.projects = parsed.projects.slice(0, MAX_RECENT_PROJECTS);
|
|
136
|
+
return parsed;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const writeRecents = (
|
|
140
|
+
service: UserDataService,
|
|
141
|
+
recents: WorkspaceSetupRecents,
|
|
142
|
+
): void => {
|
|
143
|
+
service.persistValue(
|
|
144
|
+
LEGEND_STUDIO_USER_DATA_KEY.WORKSPACE_SETUP_RECENTS,
|
|
145
|
+
WorkspaceSetupRecents.serialization.toJson(recents),
|
|
146
|
+
);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// --- Cached sandbox info -----------------------------------------------------
|
|
150
|
+
|
|
151
|
+
// Sandbox access and the sandbox project id rarely change for a given user,
|
|
152
|
+
// but they're costly to look up on every setup mount (one graph manager call
|
|
153
|
+
// + one tagged-project search). Cache the result for a day and revalidate
|
|
154
|
+
// in the background on the fast path.
|
|
155
|
+
const SANDBOX_INFO_TTL_MS = 24 * 60 * 60 * 1000;
|
|
156
|
+
|
|
157
|
+
export class CachedSandboxInfo {
|
|
158
|
+
userId!: string;
|
|
159
|
+
hasAccess!: boolean;
|
|
160
|
+
/** undefined when the user has access but hasn't created a sandbox yet. */
|
|
161
|
+
projectId?: string | undefined;
|
|
162
|
+
fetchedAt!: number;
|
|
163
|
+
|
|
164
|
+
static readonly serialization = new SerializationFactory(
|
|
165
|
+
createModelSchema(CachedSandboxInfo, {
|
|
166
|
+
fetchedAt: primitive(),
|
|
167
|
+
hasAccess: primitive(),
|
|
168
|
+
projectId: optional(primitive()),
|
|
169
|
+
userId: primitive(),
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
32
172
|
}
|
|
33
173
|
|
|
34
174
|
export class LegendStudioUserDataHelper {
|
|
@@ -70,4 +210,172 @@ export class LegendStudioUserDataHelper {
|
|
|
70
210
|
val,
|
|
71
211
|
);
|
|
72
212
|
}
|
|
213
|
+
|
|
214
|
+
// --- Workspace setup recents ---------------------------------------------
|
|
215
|
+
|
|
216
|
+
static workspaceSetup_getRecentProjects(
|
|
217
|
+
service: UserDataService,
|
|
218
|
+
): RecentProjectEntry[] {
|
|
219
|
+
return readRecents(service).projects;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
static workspaceSetup_getRecentWorkspaces(
|
|
223
|
+
service: UserDataService,
|
|
224
|
+
): RecentWorkspaceEntry[] {
|
|
225
|
+
return readRecents(service).workspaces;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
static workspaceSetup_recordRecentProject(
|
|
229
|
+
service: UserDataService,
|
|
230
|
+
entry: {
|
|
231
|
+
projectId: string;
|
|
232
|
+
name: string;
|
|
233
|
+
description: string;
|
|
234
|
+
webUrl: string;
|
|
235
|
+
tags: string[];
|
|
236
|
+
},
|
|
237
|
+
): RecentProjectEntry[] {
|
|
238
|
+
const recents = readRecents(service);
|
|
239
|
+
const next = new RecentProjectEntry();
|
|
240
|
+
next.projectId = entry.projectId;
|
|
241
|
+
next.name = entry.name;
|
|
242
|
+
next.description = entry.description;
|
|
243
|
+
next.webUrl = entry.webUrl;
|
|
244
|
+
next.tags = entry.tags;
|
|
245
|
+
next.lastOpenedAt = Date.now();
|
|
246
|
+
recents.projects = [
|
|
247
|
+
next,
|
|
248
|
+
...recents.projects.filter((p) => p.projectId !== entry.projectId),
|
|
249
|
+
].slice(0, MAX_RECENT_PROJECTS);
|
|
250
|
+
writeRecents(service, recents);
|
|
251
|
+
return recents.projects;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
static workspaceSetup_recordRecentWorkspace(
|
|
255
|
+
service: UserDataService,
|
|
256
|
+
entry: {
|
|
257
|
+
projectId: string;
|
|
258
|
+
workspaceId: string;
|
|
259
|
+
workspaceType: WorkspaceType;
|
|
260
|
+
},
|
|
261
|
+
): RecentWorkspaceEntry[] {
|
|
262
|
+
const recents = readRecents(service);
|
|
263
|
+
const next = new RecentWorkspaceEntry();
|
|
264
|
+
next.projectId = entry.projectId;
|
|
265
|
+
next.workspaceId = entry.workspaceId;
|
|
266
|
+
next.workspaceType = entry.workspaceType;
|
|
267
|
+
next.lastOpenedAt = Date.now();
|
|
268
|
+
recents.workspaces = [
|
|
269
|
+
next,
|
|
270
|
+
...recents.workspaces.filter(
|
|
271
|
+
(w) =>
|
|
272
|
+
!(
|
|
273
|
+
w.projectId === entry.projectId &&
|
|
274
|
+
w.workspaceId === entry.workspaceId &&
|
|
275
|
+
w.workspaceType === entry.workspaceType
|
|
276
|
+
),
|
|
277
|
+
),
|
|
278
|
+
].slice(0, MAX_RECENT_WORKSPACES);
|
|
279
|
+
writeRecents(service, recents);
|
|
280
|
+
return recents.workspaces;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
static workspaceSetup_removeRecentProject(
|
|
284
|
+
service: UserDataService,
|
|
285
|
+
projectId: string,
|
|
286
|
+
): WorkspaceSetupRecents {
|
|
287
|
+
const recents = readRecents(service);
|
|
288
|
+
recents.projects = recents.projects.filter(
|
|
289
|
+
(p) => p.projectId !== projectId,
|
|
290
|
+
);
|
|
291
|
+
recents.workspaces = recents.workspaces.filter(
|
|
292
|
+
(w) => w.projectId !== projectId,
|
|
293
|
+
);
|
|
294
|
+
writeRecents(service, recents);
|
|
295
|
+
return recents;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
static workspaceSetup_removeRecentWorkspace(
|
|
299
|
+
service: UserDataService,
|
|
300
|
+
entry: {
|
|
301
|
+
projectId: string;
|
|
302
|
+
workspaceId: string;
|
|
303
|
+
workspaceType: WorkspaceType;
|
|
304
|
+
},
|
|
305
|
+
): RecentWorkspaceEntry[] {
|
|
306
|
+
const recents = readRecents(service);
|
|
307
|
+
recents.workspaces = recents.workspaces.filter(
|
|
308
|
+
(w) =>
|
|
309
|
+
!(
|
|
310
|
+
w.projectId === entry.projectId &&
|
|
311
|
+
w.workspaceId === entry.workspaceId &&
|
|
312
|
+
w.workspaceType === entry.workspaceType
|
|
313
|
+
),
|
|
314
|
+
);
|
|
315
|
+
writeRecents(service, recents);
|
|
316
|
+
return recents.workspaces;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
static workspaceSetup_clearRecents(service: UserDataService): void {
|
|
320
|
+
writeRecents(service, emptyRecents());
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// --- Cached sandbox info -------------------------------------------------
|
|
324
|
+
|
|
325
|
+
static workspaceSetup_getCachedSandboxInfo(
|
|
326
|
+
service: UserDataService,
|
|
327
|
+
currentUserId: string,
|
|
328
|
+
): CachedSandboxInfo | undefined {
|
|
329
|
+
const raw = returnUndefOnError(() =>
|
|
330
|
+
service.getObjectValue(
|
|
331
|
+
LEGEND_STUDIO_USER_DATA_KEY.WORKSPACE_SETUP_SANDBOX_INFO,
|
|
332
|
+
),
|
|
333
|
+
);
|
|
334
|
+
if (!raw) {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
const parsed = returnUndefOnError(() =>
|
|
338
|
+
CachedSandboxInfo.serialization.fromJson(
|
|
339
|
+
raw as PlainObject<CachedSandboxInfo>,
|
|
340
|
+
),
|
|
341
|
+
);
|
|
342
|
+
if (!parsed) {
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
// Discard entries that don't belong to the user looking at the screen
|
|
346
|
+
// (e.g., after a user switch on a shared machine) or that have aged out.
|
|
347
|
+
if (parsed.userId !== currentUserId) {
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
if (Date.now() - parsed.fetchedAt > SANDBOX_INFO_TTL_MS) {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
return parsed;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
static workspaceSetup_recordSandboxInfo(
|
|
357
|
+
service: UserDataService,
|
|
358
|
+
info: {
|
|
359
|
+
userId: string;
|
|
360
|
+
hasAccess: boolean;
|
|
361
|
+
projectId?: string | undefined;
|
|
362
|
+
},
|
|
363
|
+
): void {
|
|
364
|
+
const entry = new CachedSandboxInfo();
|
|
365
|
+
entry.userId = info.userId;
|
|
366
|
+
entry.hasAccess = info.hasAccess;
|
|
367
|
+
entry.projectId = info.projectId;
|
|
368
|
+
entry.fetchedAt = Date.now();
|
|
369
|
+
service.persistValue(
|
|
370
|
+
LEGEND_STUDIO_USER_DATA_KEY.WORKSPACE_SETUP_SANDBOX_INFO,
|
|
371
|
+
CachedSandboxInfo.serialization.toJson(entry),
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
static workspaceSetup_clearSandboxInfo(service: UserDataService): void {
|
|
376
|
+
service.persistValue(
|
|
377
|
+
LEGEND_STUDIO_USER_DATA_KEY.WORKSPACE_SETUP_SANDBOX_INFO,
|
|
378
|
+
undefined,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
73
381
|
}
|
|
@@ -400,6 +400,7 @@ export function renderEmbeddedDataEditor(
|
|
|
400
400
|
dataState={embeddedDataState}
|
|
401
401
|
isReadOnly={isReadOnly}
|
|
402
402
|
isSharedData={isSharedData}
|
|
403
|
+
hideColumnDefinitions={true}
|
|
403
404
|
/>
|
|
404
405
|
);
|
|
405
406
|
} else if (embeddedDataState instanceof DataElementReferenceState) {
|
|
@@ -426,7 +426,8 @@ export const RelationElementEditor = observer(
|
|
|
426
426
|
</div>
|
|
427
427
|
</div>
|
|
428
428
|
) : null}
|
|
429
|
-
{embeddedData.rows.length === 0
|
|
429
|
+
{embeddedData.rows.length === 0 &&
|
|
430
|
+
embeddedData.columns.length === 0 ? (
|
|
430
431
|
<div className="relation-test-data-editor__empty-data">
|
|
431
432
|
<div className="relation-test-data-editor__empty-text">
|
|
432
433
|
No test data rows. Click "+" below to start entering
|
|
@@ -440,13 +441,20 @@ export const RelationElementEditor = observer(
|
|
|
440
441
|
<tr>
|
|
441
442
|
{embeddedData.columns.map((column, columnIndex) => (
|
|
442
443
|
<th
|
|
443
|
-
key={
|
|
444
|
+
key={`col-${guaranteeNonNullable(columnIndex)}`}
|
|
444
445
|
className="relation-test-data-editor__th"
|
|
445
446
|
>
|
|
446
447
|
<div className="relation-test-data-editor__th__inner">
|
|
447
|
-
<
|
|
448
|
-
|
|
449
|
-
|
|
448
|
+
<input
|
|
449
|
+
className="relation-test-data-editor__th__label relation-test-data-editor__th__label--editable"
|
|
450
|
+
type="text"
|
|
451
|
+
value={column}
|
|
452
|
+
onChange={(e) =>
|
|
453
|
+
updateColumn(columnIndex, e.target.value)
|
|
454
|
+
}
|
|
455
|
+
disabled={!canEditColumns}
|
|
456
|
+
title={column}
|
|
457
|
+
/>
|
|
450
458
|
<button
|
|
451
459
|
className="relation-test-data-editor__th__delete"
|
|
452
460
|
onClick={() => removeColumn(columnIndex)}
|
|
@@ -0,0 +1,181 @@
|
|
|
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 { observer } from 'mobx-react-lite';
|
|
18
|
+
import {
|
|
19
|
+
FolderIcon,
|
|
20
|
+
HistoryIcon,
|
|
21
|
+
TimesIcon,
|
|
22
|
+
UserIcon,
|
|
23
|
+
UsersIcon,
|
|
24
|
+
} from '@finos/legend-art';
|
|
25
|
+
import { WorkspaceType } from '@finos/legend-server-sdlc';
|
|
26
|
+
import type { WorkspaceSetupStore } from '../../stores/workspace-setup/WorkspaceSetupStore.js';
|
|
27
|
+
import { generateEditorRoute } from '../../__lib__/LegendStudioNavigation.js';
|
|
28
|
+
|
|
29
|
+
const MAX_TILES = 6;
|
|
30
|
+
|
|
31
|
+
const formatRelativeTime = (timestamp: number): string => {
|
|
32
|
+
const diffMs = Date.now() - timestamp;
|
|
33
|
+
if (diffMs < 0) {
|
|
34
|
+
return 'just now';
|
|
35
|
+
}
|
|
36
|
+
const minutes = Math.floor(diffMs / 60_000);
|
|
37
|
+
if (minutes < 1) {
|
|
38
|
+
return 'just now';
|
|
39
|
+
}
|
|
40
|
+
if (minutes < 60) {
|
|
41
|
+
return `${minutes}m ago`;
|
|
42
|
+
}
|
|
43
|
+
const hours = Math.floor(minutes / 60);
|
|
44
|
+
if (hours < 24) {
|
|
45
|
+
return `${hours}h ago`;
|
|
46
|
+
}
|
|
47
|
+
const days = Math.floor(hours / 24);
|
|
48
|
+
if (days < 7) {
|
|
49
|
+
return `${days}d ago`;
|
|
50
|
+
}
|
|
51
|
+
const weeks = Math.floor(days / 7);
|
|
52
|
+
if (weeks < 4) {
|
|
53
|
+
return `${weeks}w ago`;
|
|
54
|
+
}
|
|
55
|
+
const months = Math.floor(days / 30);
|
|
56
|
+
if (months < 12) {
|
|
57
|
+
return `${months}mo ago`;
|
|
58
|
+
}
|
|
59
|
+
return `${Math.floor(days / 365)}y ago`;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const RecentWorkspacesPanel = observer(
|
|
63
|
+
(props: { setupStore: WorkspaceSetupStore }) => {
|
|
64
|
+
const { setupStore } = props;
|
|
65
|
+
const applicationStore = setupStore.applicationStore;
|
|
66
|
+
|
|
67
|
+
// Recents are stored in LRU order (most-recent first). Show the top N
|
|
68
|
+
// workspaces; look up each one's cached project entry for richer tile
|
|
69
|
+
// metadata (name, description tooltip, tag badges).
|
|
70
|
+
const tiles = setupStore.recentWorkspaces.slice(0, MAX_TILES);
|
|
71
|
+
if (tiles.length === 0) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const projectById = new Map(
|
|
76
|
+
setupStore.recentProjects.map((p) => [p.projectId, p]),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const openWorkspace = (
|
|
80
|
+
projectId: string,
|
|
81
|
+
workspaceId: string,
|
|
82
|
+
workspaceType: WorkspaceType,
|
|
83
|
+
): void => {
|
|
84
|
+
applicationStore.navigationService.navigator.goToLocation(
|
|
85
|
+
generateEditorRoute(projectId, undefined, workspaceId, workspaceType),
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className="workspace-setup__recents">
|
|
91
|
+
<div className="workspace-setup__recents__header">
|
|
92
|
+
<div className="workspace-setup__recents__header__title">
|
|
93
|
+
<HistoryIcon />
|
|
94
|
+
<span>Recent workspaces</span>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
<div className="workspace-setup__recents__grid">
|
|
98
|
+
{tiles.map((entry) => {
|
|
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}`;
|
|
107
|
+
const key = `${entry.projectId}::${entry.workspaceType}::${entry.workspaceId}`;
|
|
108
|
+
const handleRemove = (
|
|
109
|
+
event: React.MouseEvent<HTMLButtonElement>,
|
|
110
|
+
): void => {
|
|
111
|
+
event.stopPropagation();
|
|
112
|
+
setupStore.removeRecentWorkspace({
|
|
113
|
+
projectId: entry.projectId,
|
|
114
|
+
workspaceId: entry.workspaceId,
|
|
115
|
+
workspaceType: entry.workspaceType,
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
return (
|
|
119
|
+
<button
|
|
120
|
+
key={key}
|
|
121
|
+
type="button"
|
|
122
|
+
className="workspace-setup__recents__tile"
|
|
123
|
+
title={tileTitle}
|
|
124
|
+
onClick={() =>
|
|
125
|
+
openWorkspace(
|
|
126
|
+
entry.projectId,
|
|
127
|
+
entry.workspaceId,
|
|
128
|
+
entry.workspaceType,
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
>
|
|
132
|
+
<button
|
|
133
|
+
type="button"
|
|
134
|
+
tabIndex={-1}
|
|
135
|
+
className="workspace-setup__recents__tile__remove"
|
|
136
|
+
title="Remove from recents"
|
|
137
|
+
onClick={handleRemove}
|
|
138
|
+
>
|
|
139
|
+
<TimesIcon />
|
|
140
|
+
</button>
|
|
141
|
+
<div className="workspace-setup__recents__tile__project">
|
|
142
|
+
<FolderIcon />
|
|
143
|
+
<span className="workspace-setup__recents__tile__project__name">
|
|
144
|
+
{projectName}
|
|
145
|
+
</span>
|
|
146
|
+
</div>
|
|
147
|
+
<div className="workspace-setup__recents__tile__workspace">
|
|
148
|
+
{entry.workspaceType === WorkspaceType.GROUP ? (
|
|
149
|
+
<UsersIcon />
|
|
150
|
+
) : (
|
|
151
|
+
<UserIcon />
|
|
152
|
+
)}
|
|
153
|
+
<span className="workspace-setup__recents__tile__workspace__name">
|
|
154
|
+
{entry.workspaceId}
|
|
155
|
+
</span>
|
|
156
|
+
</div>
|
|
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>
|
|
173
|
+
</div>
|
|
174
|
+
</button>
|
|
175
|
+
);
|
|
176
|
+
})}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
},
|
|
181
|
+
);
|