@finos/legend-application-studio
Version:
Legend Studio application core
960 lines • 51.1 kB
JavaScript
/**
* Copyright (c) 2020-present, Goldman Sachs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { action, computed, flow, flowResult, makeObservable, observable, } from 'mobx';
import { ExplorerTreeState } from './ExplorerTreeState.js';
import { ACTIVITY_MODE, PANEL_MODE, GRAPH_EDITOR_MODE, EDITOR_MODE, } from './EditorConfig.js';
import { EditorGraphState, GraphBuilderStatus, } from './EditorGraphState.js';
import { ChangeDetectionState } from './ChangeDetectionState.js';
import { NewElementState } from './NewElementState.js';
import { WorkspaceUpdaterState } from './sidebar-state/WorkspaceUpdaterState.js';
import { ProjectOverviewState } from './sidebar-state/ProjectOverviewState.js';
import { WorkspaceReviewState } from './sidebar-state/WorkspaceReviewState.js';
import { FormLocalChangesState, } from './sidebar-state/LocalChangesState.js';
import { WorkspaceWorkflowManagerState } from './sidebar-state/WorkflowManagerState.js';
import { LogEvent, isNonNullable, assertErrorThrown, guaranteeNonNullable, UnsupportedOperationError, ActionState, AssertionError, guaranteeType, returnUndefOnError, } from '@finos/legend-shared';
import { EditorSDLCState } from './EditorSDLCState.js';
import { ModelImporterState } from './editor-state/ModelImporterState.js';
import { ProjectConfigurationEditorState } from './editor-state/project-configuration-editor-state/ProjectConfigurationEditorState.js';
import { DevToolPanelState, payloadDebugger, } from './panel-group/DevToolPanelState.js';
import { generateEditorRoute, generateSetupRoute, generateViewProjectRoute, LEGEND_STUDIO_QUERY_PARAMS, } from '../../__lib__/LegendStudioNavigation.js';
import { PanelDisplayState } from '@finos/legend-art';
import { ProjectConfiguration, WorkspaceType, } from '@finos/legend-server-sdlc';
import { GraphManagerState, GRAPH_MANAGER_EVENT } from '@finos/legend-graph';
import { ActionAlertActionType, ActionAlertType, APPLICATION_EVENT, DEFAULT_TAB_SIZE, } from '@finos/legend-application';
import { LEGEND_STUDIO_APP_EVENT } from '../../__lib__/LegendStudioEvent.js';
import { StandardEditorMode } from './StandardEditorMode.js';
import { WorkspaceUpdateConflictResolutionState } from './sidebar-state/WorkspaceUpdateConflictResolutionState.js';
import { PACKAGEABLE_ELEMENT_TYPE, PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY, } from './utils/ModelClassifierUtils.js';
import { GlobalTestRunnerState } from './sidebar-state/testable/GlobalTestRunnerState.js';
import { EmbeddedQueryBuilderState } from './EmbeddedQueryBuilderState.js';
import { LEGEND_STUDIO_COMMAND_KEY } from '../../__lib__/LegendStudioCommand.js';
import { EditorTabManagerState } from './EditorTabManagerState.js';
import { GraphEditFormModeState } from './GraphEditFormModeState.js';
import { GraphEditGrammarModeState } from './GraphEditGrammarModeState.js';
import { GlobalBulkServiceRegistrationState } from './sidebar-state/BulkServiceRegistrationState.js';
import { StudioSQLPlaygroundPanelState } from './panel-group/StudioSQLPlaygroundPanelState.js';
import { LegendSQLStudioPlaygroundState } from './LegendSQLStudioPlaygroundState.js';
import { GlobalEndToEndWorkflowState } from './sidebar-state/end-to-end-workflow/GlobalEndToEndFlowState.js';
import { SHOWCASE_PANEL_LOCAL_STORAGE, toggleShowcasePanel, } from '../../components/editor/ShowcaseSideBar.js';
import { GraphEditLazyGrammarModeState, LazyTextEditorStore, } from '../lazy-text-editor/LazyTextEditorStore.js';
import { EditorInitialConfiguration } from './editor-state/element-editor-state/ElementEditorInitialConfiguration.js';
import { LakehouseIngestionManager } from '@finos/legend-server-lakehouse';
import { DevMetadataState } from './sidebar-state/dev-metadata/DevMetadataState.js';
export class EditorExtensionState {
/**
* This helps to better type-check for this empty abtract type
* See https://github.com/finos/legend-studio/blob/master/docs/technical/typescript-usage.md#understand-typescript-structual-type-system
*/
_$nominalTypeBrand;
}
export class EditorStore {
applicationStore;
sdlcServerClient;
depotServerClient;
ingestionManager;
pluginManager;
/**
* This is a mechanism to have the store holds references to extension states
* so that we can refer back to these states when needed or do cross-extensions
* operations
*/
extensionStates = [];
initState = ActionState.create();
initialEntityPath;
editorConfig;
editorMode;
// NOTE: once we clear up the editor store to make modes more separated
// we should remove these sets of functions. They are basically hacks to
// ensure hiding parts of the UI based on the editing mode.
// Instead, we will gradually move these `boolean` flags into `EditorMode`
// See https://github.com/finos/legend-studio/issues/317
mode = EDITOR_MODE.STANDARD;
// SDLC
sdlcState;
changeDetectionState;
// TODO: make EditorGraphState extend GraphMangerState and merge the state together for Studio
graphState;
graphManagerState;
graphEditorMode;
// sidebar and panel
explorerTreeState;
projectOverviewState;
workspaceWorkflowManagerState;
globalTestRunnerState;
workspaceUpdaterState;
workspaceReviewState;
localChangesState;
conflictResolutionState;
globalBulkServiceRegistrationState;
globalEndToEndWorkflowState;
devToolState;
devMetadataState;
studioSqlPlaygroundState;
legendSQLStudioPlaygroundState;
modelImporterState;
projectConfigurationEditorState;
embeddedQueryBuilderState;
embeddedDataCubeViewerState;
newElementState;
/**
* Since we want to share element generation state across all element in the editor, we will create 1 element generate state
* per file generation configuration type.
*/
elementGenerationStates = [];
showSearchElementCommand = false;
quickInputState;
activePanelMode = PANEL_MODE.CONSOLE;
panelGroupDisplayState = new PanelDisplayState({
initial: 0,
default: 300,
snap: 100,
});
activeActivity = ACTIVITY_MODE.EXPLORER;
sideBarDisplayState = new PanelDisplayState({
initial: 300,
default: 300,
snap: 150,
});
showcasePanelDisplayState;
showcaseDefaultSize = 500;
tabManagerState = new EditorTabManagerState(this);
supportedElementTypesWithCategory;
lazyTextEditorStore = new LazyTextEditorStore(this);
constructor(applicationStore, sdlcServerClient, depotServerClient) {
makeObservable(this, {
editorMode: observable,
mode: observable,
activePanelMode: observable,
activeActivity: observable,
graphEditorMode: observable,
showSearchElementCommand: observable,
quickInputState: observable,
lazyTextEditorStore: observable,
isInViewerMode: computed,
disableGraphEditing: computed,
isInConflictResolutionMode: computed,
isInitialized: computed,
setEditorMode: action,
setMode: action,
setActivePanelMode: action,
cleanUp: action,
reset: action,
setActiveActivity: action,
setShowSearchElementCommand: action,
setQuickInputState: action,
initialize: flow,
initMode: flow,
initStandardMode: flow,
initializeLazyTextMode: flow,
initConflictResolutionMode: flow,
buildGraph: flow,
toggleTextMode: flow,
switchModes: flow,
embeddedDataCubeViewerState: observable,
setEmbeddedDataCubeViewerState: action,
});
this.applicationStore = applicationStore;
this.sdlcServerClient = sdlcServerClient;
this.depotServerClient = depotServerClient;
this.pluginManager = applicationStore.pluginManager;
this.editorMode = new StandardEditorMode(this);
this.sdlcState = new EditorSDLCState(this);
this.graphState = new EditorGraphState(this);
this.graphManagerState = new GraphManagerState(applicationStore.pluginManager, applicationStore.logService);
this.graphEditorMode = new GraphEditFormModeState(this);
this.changeDetectionState = new ChangeDetectionState(this, this.graphState);
this.devToolState = new DevToolPanelState(this);
this.devMetadataState = new DevMetadataState(this);
this.studioSqlPlaygroundState = new StudioSQLPlaygroundPanelState(this);
this.legendSQLStudioPlaygroundState = new LegendSQLStudioPlaygroundState(this);
this.embeddedQueryBuilderState = new EmbeddedQueryBuilderState(this);
// side bar panels
this.explorerTreeState = new ExplorerTreeState(this);
this.projectOverviewState = new ProjectOverviewState(this, this.sdlcState);
this.globalTestRunnerState = new GlobalTestRunnerState(this, this.sdlcState);
this.globalEndToEndWorkflowState = new GlobalEndToEndWorkflowState(this);
this.workspaceWorkflowManagerState = new WorkspaceWorkflowManagerState(this, this.sdlcState);
this.workspaceUpdaterState = new WorkspaceUpdaterState(this, this.sdlcState);
this.workspaceReviewState = new WorkspaceReviewState(this, this.sdlcState);
this.localChangesState = new FormLocalChangesState(this, this.sdlcState);
this.conflictResolutionState = new WorkspaceUpdateConflictResolutionState(this, this.sdlcState);
this.newElementState = new NewElementState(this);
this.globalBulkServiceRegistrationState =
new GlobalBulkServiceRegistrationState(this, this.sdlcState);
// special (singleton) editors
this.modelImporterState = new ModelImporterState(this);
this.projectConfigurationEditorState = new ProjectConfigurationEditorState(this, this.sdlcState);
// ingestion
const ingestionConifg = applicationStore.config.options.ingestDeploymentConfig;
if (ingestionConifg) {
this.ingestionManager = new LakehouseIngestionManager(ingestionConifg.discoveryUrl, ingestionConifg.deployment.defaultServer, ingestionConifg.deployment.useDefaultServer, this.applicationStore.tracerService);
}
// extensions
this.extensionStates = this.pluginManager
.getApplicationPlugins()
.flatMap((plugin) => plugin.getExtraEditorExtensionStateBuilders?.() ?? [])
.map((creator) => creator(this))
.filter(isNonNullable);
this.supportedElementTypesWithCategory =
this.getSupportedElementTypesWithCategory();
this.showcasePanelDisplayState = new PanelDisplayState({
initial: this.showcaseInitialSize,
default: this.showcaseDefaultSize,
snap: 150,
});
}
get showcaseInitialSize() {
const showcasesSavedAsOpen = this.applicationStore.userDataService.getBooleanValue(SHOWCASE_PANEL_LOCAL_STORAGE.PANEL_STATE_KEY);
const showcaseEnabled = this.applicationStore.config.showcaseServerUrl;
if (showcaseEnabled &&
(showcasesSavedAsOpen || showcasesSavedAsOpen === undefined)) {
return this.showcaseDefaultSize;
}
else {
return 0;
}
}
get isInitialized() {
if (this.isInViewerMode) {
return (this.editorMode.isInitialized &&
this.graphManagerState.systemBuildState.hasSucceeded);
}
else {
return (Boolean(this.sdlcState.currentProject &&
this.sdlcState.currentWorkspace &&
this.sdlcState.currentRevision &&
this.sdlcState.remoteWorkspaceRevision) && this.graphManagerState.systemBuildState.hasSucceeded);
}
}
get isInViewerMode() {
return this.mode === EDITOR_MODE.VIEWER;
}
get disableGraphEditing() {
return this.isInViewerMode && this.editorMode.disableEditing;
}
get isInConflictResolutionMode() {
return this.mode === EDITOR_MODE.CONFLICT_RESOLUTION;
}
setEditorMode(val) {
this.editorMode = val;
}
setMode(val) {
this.mode = val;
}
setShowSearchElementCommand(val) {
this.showSearchElementCommand = val;
}
setEmbeddedDataCubeViewerState(val) {
this.embeddedDataCubeViewerState = val;
}
setQuickInputState(val) {
this.quickInputState = val;
}
cleanUp() {
// dismiss all the alerts as these are parts of application, if we don't do this, we might
// end up blocking other parts of the app
// e.g. trying going to an unknown workspace, we will be redirected to the home page
// but the blocking alert for not-found workspace will still block the app
this.applicationStore.alertService.setBlockingAlert(undefined);
this.applicationStore.alertService.setActionAlertInfo(undefined);
// stop change detection to avoid memory-leak
this.changeDetectionState.stop();
}
/**
* TODO?: we should really think of how we could simplify the trigger condition below
* after we refactor editor modes
*
* See https://github.com/finos/legend-studio/issues/317
*/
createEditorCommandTrigger(additionalChecker) {
return () =>
// we don't want to leak any hotkeys when we have embedded query builder open
// TODO?: we probably should come up with a more generic mechanism for this
!this.embeddedQueryBuilderState.queryBuilderState &&
(!additionalChecker || additionalChecker());
}
registerCommands() {
this.applicationStore.commandService.registerCommand({
key: LEGEND_STUDIO_COMMAND_KEY.COMPILE,
trigger: this.createEditorCommandTrigger(() => this.isInitialized &&
(!this.isInConflictResolutionMode ||
this.conflictResolutionState.hasResolvedAllConflicts)),
action: () => {
flowResult(this.graphEditorMode.globalCompile()).catch(this.applicationStore.alertUnhandledError);
},
});
this.applicationStore.commandService.registerCommand({
key: LEGEND_STUDIO_COMMAND_KEY.GENERATE,
trigger: this.createEditorCommandTrigger(() => this.isInitialized &&
(!this.isInConflictResolutionMode ||
this.conflictResolutionState.hasResolvedAllConflicts)),
action: () => {
flowResult(this.graphState.graphGenerationState.globalGenerate()).catch(this.applicationStore.alertUnhandledError);
},
});
this.applicationStore.commandService.registerCommand({
key: LEGEND_STUDIO_COMMAND_KEY.CREATE_ELEMENT,
trigger: this.createEditorCommandTrigger(() => !this.isInViewerMode),
action: () => this.newElementState.openModal(),
});
this.applicationStore.commandService.registerCommand({
key: LEGEND_STUDIO_COMMAND_KEY.SEARCH_ELEMENT,
trigger: this.createEditorCommandTrigger(),
action: () => this.setShowSearchElementCommand(!this.showSearchElementCommand),
});
this.applicationStore.commandService.registerCommand({
key: LEGEND_STUDIO_COMMAND_KEY.TOGGLE_TEXT_MODE,
trigger: this.createEditorCommandTrigger(() => this.isInitialized &&
(!this.isInConflictResolutionMode ||
this.conflictResolutionState.hasResolvedAllConflicts)),
action: () => {
flowResult(this.toggleTextMode()).catch(this.applicationStore.alertUnhandledError);
},
});
this.applicationStore.commandService.registerCommand({
key: LEGEND_STUDIO_COMMAND_KEY.OPEN_SHOWCASES,
trigger: this.createEditorCommandTrigger(() => this.isInitialized &&
(!this.isInConflictResolutionMode ||
this.conflictResolutionState.hasResolvedAllConflicts)),
action: () => {
toggleShowcasePanel(this);
},
});
this.applicationStore.commandService.registerCommand({
key: LEGEND_STUDIO_COMMAND_KEY.TOGGLE_MODEL_LOADER,
trigger: this.createEditorCommandTrigger(() => !this.isInViewerMode),
action: () => this.tabManagerState.openTab(this.modelImporterState),
});
this.applicationStore.commandService.registerCommand({
key: LEGEND_STUDIO_COMMAND_KEY.SYNC_WITH_WORKSPACE,
trigger: this.createEditorCommandTrigger(() => !this.isInViewerMode),
action: () => {
flowResult(this.localChangesState.pushLocalChanges()).catch(this.applicationStore.alertUnhandledError);
},
});
this.applicationStore.commandService.registerCommand({
key: LEGEND_STUDIO_COMMAND_KEY.TOGGLE_PANEL_GROUP,
trigger: this.createEditorCommandTrigger(() => !this.isInViewerMode),
action: () => this.panelGroupDisplayState.toggle(),
});
this.applicationStore.commandService.registerCommand({
key: LEGEND_STUDIO_COMMAND_KEY.TOGGLE_SIDEBAR_EXPLORER,
trigger: this.createEditorCommandTrigger(),
action: () => this.setActiveActivity(ACTIVITY_MODE.EXPLORER),
});
this.applicationStore.commandService.registerCommand({
key: LEGEND_STUDIO_COMMAND_KEY.TOGGLE_SIDEBAR_LOCAL_CHANGES,
trigger: this.createEditorCommandTrigger(() => !this.isInViewerMode),
action: () => this.setActiveActivity(ACTIVITY_MODE.LOCAL_CHANGES),
});
this.applicationStore.commandService.registerCommand({
key: LEGEND_STUDIO_COMMAND_KEY.TOGGLE_SIDEBAR_WORKSPACE_REVIEW,
trigger: this.createEditorCommandTrigger(() => !this.isInViewerMode),
action: () => this.setActiveActivity(ACTIVITY_MODE.WORKSPACE_REVIEW),
});
this.applicationStore.commandService.registerCommand({
key: LEGEND_STUDIO_COMMAND_KEY.TOGGLE_SIDEBAR_WORKSPACE_UPDATER,
trigger: this.createEditorCommandTrigger(() => !this.isInViewerMode),
action: () => this.setActiveActivity(ACTIVITY_MODE.WORKSPACE_UPDATER),
});
}
deregisterCommands() {
[
LEGEND_STUDIO_COMMAND_KEY.SYNC_WITH_WORKSPACE,
LEGEND_STUDIO_COMMAND_KEY.CREATE_ELEMENT,
LEGEND_STUDIO_COMMAND_KEY.SEARCH_ELEMENT,
LEGEND_STUDIO_COMMAND_KEY.TOGGLE_TEXT_MODE,
LEGEND_STUDIO_COMMAND_KEY.GENERATE,
LEGEND_STUDIO_COMMAND_KEY.COMPILE,
LEGEND_STUDIO_COMMAND_KEY.TOGGLE_PANEL_GROUP,
LEGEND_STUDIO_COMMAND_KEY.TOGGLE_MODEL_LOADER,
LEGEND_STUDIO_COMMAND_KEY.TOGGLE_SIDEBAR_EXPLORER,
LEGEND_STUDIO_COMMAND_KEY.TOGGLE_SIDEBAR_LOCAL_CHANGES,
LEGEND_STUDIO_COMMAND_KEY.TOGGLE_SIDEBAR_WORKSPACE_REVIEW,
LEGEND_STUDIO_COMMAND_KEY.TOGGLE_SIDEBAR_WORKSPACE_UPDATER,
].forEach((key) => this.applicationStore.commandService.deregisterCommand(key));
}
reset() {
this.tabManagerState.closeAllTabs();
this.projectConfigurationEditorState = new ProjectConfigurationEditorState(this, this.sdlcState);
this.explorerTreeState = new ExplorerTreeState(this);
}
deCodeStudioQueryParams(studioParams) {
if (this.editorConfig) {
// If editor config is already set, we do not decode again
return;
}
if (!studioParams) {
// If studio params are not provided, we do not decode
return;
}
const editorConfig = studioParams[LEGEND_STUDIO_QUERY_PARAMS.EDITOR_CONFIG];
if (!editorConfig) {
return;
}
const _config = returnUndefOnError(() => EditorInitialConfiguration.serialization.fromJson(JSON.parse(atob(editorConfig))));
const config = guaranteeNonNullable(_config, `error reading and serializing config ${editorConfig}`);
this.editorConfig = config;
}
internalizeEntityPath(params, studioParams) {
const { projectId, entityPath } = params;
const workspaceType = params.groupWorkspaceId
? WorkspaceType.GROUP
: WorkspaceType.USER;
const workspaceId = guaranteeNonNullable(params.groupWorkspaceId ?? params.workspaceId, `Workspace/group workspace ID is not provided`);
if (entityPath) {
this.initialEntityPath = entityPath;
this.deCodeStudioQueryParams(studioParams);
this.applicationStore.navigationService.navigator.updateCurrentLocation(generateEditorRoute(guaranteeNonNullable(projectId), params.patchReleaseVersionId, workspaceId, workspaceType));
}
}
/**
* This is the entry of the app logic where the initialization of editor states happens
* Here, we ensure the order of calls after checking existence of current project and workspace
* If either of them does not exist, we cannot proceed.
*/
*initialize(projectId, patchReleaseVersionId, workspaceId, workspaceType, studioParams) {
if (!this.initState.isInInitialState) {
/**
* Since React `fast-refresh` will sometimes cause `Editor` to rerender, this method will be called again
* as all hooks are recalled, as such, ONLY IN DEVELOPMENT mode we allow this to not fail-fast
* we also have to `undo` some of what the `cleanUp` does to this store as the cleanup part of all hooks
* will be triggered as well
*/
// eslint-disable-next-line no-process-env
if (process.env.NODE_ENV === 'development') {
this.applicationStore.logService.debug(LogEvent.create(APPLICATION_EVENT.DEBUG), `Fast-refreshing the app - undoing cleanUp() and preventing initialize() recall in editor store...`);
this.changeDetectionState.start();
return;
}
// eslint-disable-next-line no-process-env
if (process.env.NODE_ENV === 'production') {
// TEMPORARY suppress until we investigate
this.applicationStore.logService.debug(LogEvent.create(APPLICATION_EVENT.DEBUG), 'Editor store is re-initialized');
}
else {
this.applicationStore.logService.debug(LogEvent.create(APPLICATION_EVENT.DEBUG), 'Editor store is re-initialized');
}
return;
}
this.initState.inProgress();
this.deCodeStudioQueryParams(studioParams);
// TODO: when we genericize the way to initialize an application page
this.applicationStore.assistantService.setIsHidden(false);
const onLeave = (hasBuildSucceeded) => {
this.initState.complete(hasBuildSucceeded);
this.initState.setMessage(undefined);
};
this.initState.setMessage(`Setting up workspace...`);
yield flowResult(this.sdlcState.fetchCurrentProject(projectId, {
suppressNotification: true,
}));
if (!this.sdlcState.currentProject) {
// If the project is not found or the user does not have access to it,
// we will not automatically redirect them to the setup page as they will lose the URL
// instead, we give them the option to:
// - reload the page (in case they later gain access)
// - back to the setup page
this.applicationStore.alertService.setActionAlertInfo({
message: `Project not found or inaccessible`,
prompt: 'Please check that the project exists and request access to it',
type: ActionAlertType.STANDARD,
actions: [
{
label: 'Reload application',
default: true,
type: ActionAlertActionType.STANDARD,
handler: () => {
this.applicationStore.navigationService.navigator.reload();
},
},
{
label: 'Back to workspace setup',
type: ActionAlertActionType.STANDARD,
handler: () => {
this.applicationStore.navigationService.navigator.goToLocation(generateSetupRoute(undefined, undefined));
},
},
],
});
onLeave(false);
return;
}
yield flowResult(this.sdlcState.fetchCurrentPatch(projectId, patchReleaseVersionId, {
suppressNotification: true,
}));
yield flowResult(this.sdlcState.fetchCurrentWorkspace(projectId, patchReleaseVersionId, workspaceId, workspaceType, {
suppressNotification: true,
}));
if (!this.sdlcState.currentWorkspace) {
// If the workspace is not found,
// we will not automatically redirect the user to the setup page as they will lose the URL
// instead, we give them the option to:
// - create the workspace
// - view project
// - back to the setup page
const createWorkspaceAndRelaunch = async () => {
try {
this.applicationStore.alertService.setBlockingAlert({
message: 'Creating workspace...',
prompt: 'Please do not close the application',
});
const workspace = await this.sdlcServerClient.createWorkspace(projectId, patchReleaseVersionId, workspaceId, workspaceType);
this.applicationStore.alertService.setBlockingAlert(undefined);
this.applicationStore.notificationService.notifySuccess(`Workspace '${workspace.workspaceId}' is succesfully created. Reloading application...`);
this.applicationStore.navigationService.navigator.reload();
}
catch (error) {
assertErrorThrown(error);
this.applicationStore.logService.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.WORKSPACE_SETUP_FAILURE), error);
this.applicationStore.notificationService.notifyError(error);
}
};
this.applicationStore.alertService.setActionAlertInfo({
message: 'Workspace not found',
prompt: `Please note that you can check out the project in viewer mode. Workspace is only required if you need to work on the project.`,
type: ActionAlertType.STANDARD,
actions: [
{
label: 'View project',
default: true,
type: ActionAlertActionType.STANDARD,
handler: () => {
this.applicationStore.navigationService.navigator.goToLocation(generateViewProjectRoute(projectId));
},
},
{
label: 'Create workspace',
type: ActionAlertActionType.STANDARD,
handler: () => {
createWorkspaceAndRelaunch().catch(this.applicationStore.alertUnhandledError);
},
},
{
label: 'Back to workspace setup',
type: ActionAlertActionType.STANDARD,
handler: () => {
this.applicationStore.navigationService.navigator.goToLocation(generateSetupRoute(projectId, workspaceId, workspaceType));
},
},
],
});
onLeave(false);
return;
}
yield Promise.all([
this.sdlcState.fetchCurrentRevision(projectId, this.sdlcState.activeWorkspace),
this.graphManagerState.graphManager.initialize({
env: this.applicationStore.config.env,
tabSize: DEFAULT_TAB_SIZE,
clientConfig: {
baseUrl: this.editorConfig?.engineServerUrl ??
this.applicationStore.config.engineServerUrl,
queryBaseUrl: this.editorConfig?.engineQueryServerUrl ??
this.applicationStore.config.engineQueryServerUrl,
enableCompression: true,
payloadDebugger,
},
}, {
tracerService: this.applicationStore.tracerService,
}),
]);
yield this.graphManagerState.initializeSystem();
yield flowResult(this.initMode());
onLeave(true);
}
*initMode() {
switch (this.mode) {
case EDITOR_MODE.STANDARD:
yield flowResult(this.initStandardMode());
return;
case EDITOR_MODE.CONFLICT_RESOLUTION:
yield flowResult(this.initConflictResolutionMode());
return;
case EDITOR_MODE.LAZY_TEXT_EDITOR:
yield flowResult(this.initializeLazyTextMode());
return;
default:
throw new UnsupportedOperationError(`Can't initialize editor for unsupported mode '${this.mode}'`);
}
}
*initStandardMode() {
const projectId = this.sdlcState.activeProject.projectId;
const activeWorkspace = this.sdlcState.activeWorkspace;
const projectConfiguration = (yield this.sdlcServerClient.getConfiguration(projectId, activeWorkspace));
this.projectConfigurationEditorState.setProjectConfiguration(ProjectConfiguration.serialization.fromJson(projectConfiguration));
// make sure we set the original project configuration to a different object
this.projectConfigurationEditorState.setOriginalProjectConfiguration(ProjectConfiguration.serialization.fromJson(projectConfiguration));
yield Promise.all([
this.buildGraph(),
this.sdlcState.checkIfWorkspaceIsOutdated(),
this.workspaceReviewState.fetchCurrentWorkspaceReview(),
this.workspaceUpdaterState.fetchLatestCommittedReviews(),
this.projectConfigurationEditorState.fetchLatestProjectStructureVersion(),
this.graphState.graphGenerationState.globalFileGenerationState.fetchAvailableFileGenerationDescriptions(),
this.graphState.graphGenerationState.externalFormatState.fetchExternalFormatDescriptions(),
this.graphState.fetchAvailableFunctionActivatorConfigurations(),
this.graphState.fetchAvailableRelationalDatabseTypeConfigurations(),
this.sdlcState.fetchProjectVersions(),
this.sdlcState.fetchPublishedProjectVersions(),
this.sdlcState.fetchAuthorizedActions(),
]);
}
*initializeLazyTextMode() {
// set up
const projectId = this.sdlcState.activeProject.projectId;
const activeWorkspace = this.sdlcState.activeWorkspace;
const projectConfiguration = (yield this.sdlcServerClient.getConfiguration(projectId, activeWorkspace));
this.projectConfigurationEditorState.setProjectConfiguration(ProjectConfiguration.serialization.fromJson(projectConfiguration));
// make sure we set the original project configuration to a different object
this.projectConfigurationEditorState.setOriginalProjectConfiguration(ProjectConfiguration.serialization.fromJson(projectConfiguration));
const startTime = Date.now();
let entities;
this.initState.setMessage(`Fetching entities...`);
try {
entities = (yield this.sdlcServerClient.getEntities(projectId, activeWorkspace));
this.changeDetectionState.workspaceLocalLatestRevisionState.setEntities(entities);
this.applicationStore.logService.info(LogEvent.create(GRAPH_MANAGER_EVENT.FETCH_GRAPH_ENTITIES__SUCCESS), Date.now() - startTime, 'ms');
}
catch (error) {
assertErrorThrown(error);
this.applicationStore.logService.error(LogEvent.create(GRAPH_MANAGER_EVENT.FETCH_GRAPH_ENTITIES_ERROR), Date.now() - startTime, 'ms');
this.applicationStore.notificationService.notifyError(error);
return;
}
finally {
this.initState.setMessage(undefined);
}
this.initState.setMessage('Building entities hash...');
yield flowResult(this.changeDetectionState.workspaceLocalLatestRevisionState.buildEntityHashesIndex(entities, LogEvent.create(LEGEND_STUDIO_APP_EVENT.CHANGE_DETECTION_BUILD_LOCAL_HASHES_INDEX__SUCCESS)));
this.initState.setMessage('Building strict lazy graph...');
(yield flowResult(this.graphState.buildGraphForLazyText()));
this.graphManagerState.graphBuildState.sync(ActionState.create().pass());
this.graphManagerState.generationsBuildState.sync(ActionState.create().pass());
this.initState.setMessage(undefined);
// switch to text mode
const graphEditorMode = new GraphEditLazyGrammarModeState(this);
try {
const editorGrammar = (yield this.graphManagerState.graphManager.entitiesToPureCode(this.changeDetectionState.workspaceLocalLatestRevisionState.entities, { pretty: true }));
yield flowResult(graphEditorMode.grammarTextEditorState.setGraphGrammarText(editorGrammar));
this.graphEditorMode = graphEditorMode;
yield flowResult(this.graphEditorMode.initialize({
useStoredEntities: true,
}));
}
catch (error) {
assertErrorThrown(error);
this.applicationStore.notificationService.notifyWarning(`Can't initialize strict text mode. Issue converting entities to grammar: ${error.message}`);
this.applicationStore.alertService.setBlockingAlert(undefined);
return;
}
}
*initConflictResolutionMode() {
yield flowResult(this.conflictResolutionState.initProjectConfigurationInConflictResolutionMode());
this.applicationStore.alertService.setActionAlertInfo({
message: 'Failed to update workspace.',
prompt: 'You can discard all of your changes or review them, resolve all merge conflicts and fix any potential compilation issues as well as test failures',
type: ActionAlertType.CAUTION,
actions: [
{
label: 'Discard your changes',
type: ActionAlertActionType.PROCEED_WITH_CAUTION,
handler: () => {
this.setActiveActivity(ACTIVITY_MODE.CONFLICT_RESOLUTION);
flowResult(this.conflictResolutionState.discardConflictResolutionChanges()).catch((error) => this.applicationStore.alertUnhandledError(error));
},
},
{
label: 'Resolve merge conflicts',
default: true,
type: ActionAlertActionType.STANDARD,
},
],
});
yield Promise.all([
this.conflictResolutionState.initialize(),
this.sdlcState.checkIfWorkspaceIsOutdated(),
this.projectConfigurationEditorState.fetchLatestProjectStructureVersion(),
this.graphState.graphGenerationState.globalFileGenerationState.fetchAvailableFileGenerationDescriptions(),
this.graphState.graphGenerationState.externalFormatState.fetchExternalFormatDescriptions(),
this.graphState.fetchAvailableFunctionActivatorConfigurations(),
this.graphState.fetchAvailableRelationalDatabseTypeConfigurations(),
this.sdlcState.fetchProjectVersions(),
this.sdlcState.fetchPublishedProjectVersions(),
this.sdlcState.fetchAuthorizedActions(),
]);
}
*buildGraph(graphEntities) {
const startTime = Date.now();
let entities;
this.initState.setMessage(`Fetching entities...`);
try {
// fetch workspace entities and config at the same time
const projectId = this.sdlcState.activeProject.projectId;
const activeWorkspace = this.sdlcState.activeWorkspace;
entities = (yield this.sdlcServerClient.getEntities(projectId, activeWorkspace));
this.changeDetectionState.workspaceLocalLatestRevisionState.setEntities(entities);
this.applicationStore.logService.info(LogEvent.create(GRAPH_MANAGER_EVENT.FETCH_GRAPH_ENTITIES__SUCCESS), Date.now() - startTime, 'ms');
}
catch (error) {
assertErrorThrown(error);
this.applicationStore.logService.error(LogEvent.create(GRAPH_MANAGER_EVENT.FETCH_GRAPH_ENTITIES_ERROR), Date.now() - startTime, 'ms');
this.applicationStore.notificationService.notifyError(error);
return;
}
finally {
this.initState.setMessage(undefined);
}
try {
const result = (yield flowResult(
// NOTE: if graph entities are provided, we will use that to build the graph.
// We use this method as a way to fully reset the application with the entities, but we still use
// the workspace entities for hashing as those are the base entities.
this.graphState.buildGraph(graphEntities ?? entities)));
if (result.error) {
if (result.status === GraphBuilderStatus.REDIRECTED_TO_TEXT_MODE) {
yield flowResult(this.changeDetectionState.workspaceLocalLatestRevisionState.buildEntityHashesIndex(entities, LogEvent.create(LEGEND_STUDIO_APP_EVENT.CHANGE_DETECTION_BUILD_LOCAL_HASHES_INDEX__SUCCESS)));
}
return;
}
this.initState.setMessage(`Starting change detection engine...`);
// build explorer tree
this.explorerTreeState.buildImmutableModelTrees();
this.explorerTreeState.build();
// open element if provided an element path
if (this.graphManagerState.graphBuildState.hasSucceeded &&
this.explorerTreeState.buildState.hasCompleted &&
this.initialEntityPath) {
try {
this.graphEditorMode.openElement(this.graphManagerState.graph.getElement(this.initialEntityPath), this.editorConfig);
}
catch {
const elementPath = this.initialEntityPath;
this.initialEntityPath = undefined;
throw new AssertionError(`Can't find element with path '${elementPath}'`);
}
}
// ======= (RE)START CHANGE DETECTION =======
this.changeDetectionState.stop();
yield flowResult(this.changeDetectionState.observeGraph());
yield Promise.all([
this.changeDetectionState.preComputeGraphElementHashes(), // for local changes detection
this.changeDetectionState.workspaceLocalLatestRevisionState.buildEntityHashesIndex(entities, LogEvent.create(LEGEND_STUDIO_APP_EVENT.CHANGE_DETECTION_BUILD_LOCAL_HASHES_INDEX__SUCCESS)),
this.sdlcState.buildWorkspaceBaseRevisionEntityHashesIndex(),
this.sdlcState.buildProjectLatestRevisionEntityHashesIndex(),
]);
this.changeDetectionState.start();
yield Promise.all([
this.changeDetectionState.computeAggregatedWorkspaceChanges(true),
this.changeDetectionState.computeAggregatedProjectLatestChanges(true),
]);
this.applicationStore.logService.info(LogEvent.create(LEGEND_STUDIO_APP_EVENT.CHANGE_DETECTION_RESTART__SUCCESS), '[ASNYC]');
// ======= FINISHED (RE)START CHANGE DETECTION =======
}
catch (error) {
assertErrorThrown(error);
this.applicationStore.logService.error(LogEvent.create(GRAPH_MANAGER_EVENT.GRAPH_BUILDER_FAILURE), error);
// since errors have been handled accordingly, we don't need to do anything here
return;
}
finally {
this.initState.setMessage(undefined);
}
}
setActiveActivity(activity, options) {
if (!this.sideBarDisplayState.isOpen) {
this.sideBarDisplayState.open();
}
else if (activity === this.activeActivity &&
!options?.keepShowingIfMatchedCurrent) {
this.sideBarDisplayState.close();
}
this.activeActivity = activity;
}
setActivePanelMode(val) {
this.activePanelMode = val;
}
*toggleTextMode() {
if (this.graphState.checkIfApplicationUpdateOperationIsRunning()) {
return;
}
if (this.graphEditorMode.disableLeaveMode) {
this.graphEditorMode.onLeave();
return;
}
if (this.graphEditorMode instanceof GraphEditFormModeState) {
this.applicationStore.alertService.setBlockingAlert({
message: 'Switching to text mode...',
showLoading: true,
});
yield flowResult(this.switchModes(GRAPH_EDITOR_MODE.GRAMMAR_TEXT));
}
else if (this.graphEditorMode instanceof GraphEditGrammarModeState) {
yield flowResult(this.switchModes(GRAPH_EDITOR_MODE.FORM));
}
else {
throw new UnsupportedOperationError('Editor only support form mode and text mode at the moment');
}
}
getSupportedElementTypesWithCategory() {
const elementTypesWithCategoryMap = new Map();
Object.values(PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY).forEach((value) => {
switch (value) {
case PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.MODEL: {
const elements = [
PACKAGEABLE_ELEMENT_TYPE.PACKAGE,
PACKAGEABLE_ELEMENT_TYPE.CLASS,
PACKAGEABLE_ELEMENT_TYPE.ASSOCIATION,
PACKAGEABLE_ELEMENT_TYPE.ENUMERATION,
PACKAGEABLE_ELEMENT_TYPE.PROFILE,
PACKAGEABLE_ELEMENT_TYPE.FUNCTION,
PACKAGEABLE_ELEMENT_TYPE.MEASURE,
PACKAGEABLE_ELEMENT_TYPE.DATA,
];
elementTypesWithCategoryMap.set(PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.MODEL, elements);
break;
}
case PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.STORE: {
const elements = [
PACKAGEABLE_ELEMENT_TYPE.DATABASE,
PACKAGEABLE_ELEMENT_TYPE.FLAT_DATA_STORE,
];
elementTypesWithCategoryMap.set(PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.STORE, elements);
break;
}
case PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.QUERY: {
const elements = [
PACKAGEABLE_ELEMENT_TYPE.CONNECTION,
PACKAGEABLE_ELEMENT_TYPE.RUNTIME,
PACKAGEABLE_ELEMENT_TYPE.MAPPING,
PACKAGEABLE_ELEMENT_TYPE.SERVICE,
PACKAGEABLE_ELEMENT_TYPE._DATA_PRODUCT,
this.applicationStore.config.options
.TEMPORARY__enableLocalConnectionBuilder
? PACKAGEABLE_ELEMENT_TYPE.TEMPORARY__LOCAL_CONNECTION
: undefined,
];
elementTypesWithCategoryMap.set(PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.QUERY, elements.filter(isNonNullable));
break;
}
// for displaying categories in order
case PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.EXTERNAL_FORMAT: {
elementTypesWithCategoryMap.set(PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.EXTERNAL_FORMAT, []);
break;
}
case PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.GENERATION: {
const elements = [
PACKAGEABLE_ELEMENT_TYPE.FILE_GENERATION,
PACKAGEABLE_ELEMENT_TYPE.GENERATION_SPECIFICATION,
];
elementTypesWithCategoryMap.set(PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.GENERATION, elements);
break;
}
// for displaying categories in order
case PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.OTHER: {
elementTypesWithCategoryMap.set(PACKAGEABLE_ELEMENT_GROUP_BY_CATEGORY.OTHER, []);
break;
}
default:
break;
}
});
const extensions = this.pluginManager
.getApplicationPlugins()
.flatMap((plugin) => plugin.getExtraSupportedElementTypesWithCategory?.() ??
new Map());
const elementTypesWithCategoryMapFromExtensions = new Map();
extensions.forEach((typeCategoryMap) => {
Array.from(typeCategoryMap.entries()).forEach((entry) => {
const [key, value] = entry;
elementTypesWithCategoryMapFromExtensions.set(key, elementTypesWithCategoryMapFromExtensions.get(key) === undefined
? [...value]
: [
...guaranteeNonNullable(elementTypesWithCategoryMapFromExtensions.get(key)),
...value,
]);
});
});
// sort extensions alphabetically and insert extensions into the base elementTypesWithCategoryMap
Array.from(elementTypesWithCategoryMapFromExtensions.entries()).forEach((entry) => {
const [key, value] = entry;
value.sort((a, b) => a.localeCompare(b));
const existingValues = elementTypesWithCategoryMap.get(key);
elementTypesWithCategoryMap.set(key, existingValues === undefined
? [...value]
: [...guaranteeNonNullable(existingValues), ...value]);
});
return elementTypesWithCategoryMap;
}
getSupportedElementTypes() {
return [
PACKAGEABLE_ELEMENT_TYPE.CLASS,
PACKAGEABLE_ELEMENT_TYPE.ENUMERATION,
PACKAGEABLE_ELEMENT_TYPE.PROFILE,
PACKAGEABLE_ELEMENT_TYPE.ASSOCIATION,
PACKAGEABLE_ELEMENT_TYPE.FUNCTION,
PACKAGEABLE_ELEMENT_TYPE.MEASURE,
].concat([
PACKAGEABLE_ELEMENT_TYPE.MAPPING,
PACKAGEABLE_ELEMENT_TYPE.RUNTIME,
PACKAGEABLE_ELEMENT_TYPE.CONNECTION,
PACKAGEABLE_ELEMENT_TYPE.SERVICE,
PACKAGEABLE_ELEMENT_TYPE.GENERATION_SPECIFICATION,
PACKAGEABLE_ELEMENT_TYPE.FILE_GENERATION,
PACKAGEABLE_ELEMENT_TYPE.FLAT_DATA_STORE,
PACKAGEABLE_ELEMENT_TYPE.DATABASE,
PACKAGEABLE_ELEMENT_TYPE.DATA,
this.applicationStore.config.options
.TEMPORARY__enableLocalConnectionBuilder
? PACKAGEABLE_ELEMENT_TYPE.TEMPORARY__LOCAL_CONNECTION
: undefined,
]
.filter(isNonNullable)
.concat(this.pluginManager
.getApplicationPlugins()
.flatMap((plugin) => plugin.getExtraSupportedElementTypes?.() ?? []))
.sort((a, b) => a.localeCompare(b)));
}
*switchModes(to, fallbackOptions) {
switch (to) {
case GRAPH_EDITOR_MODE.GRAMMAR_TEXT: {
const graphEditorMode = new GraphEditGrammarModeState(this);
try {
yield flowResult(this.graphEditorMode.onLeave(fallbackOptions));
yield flowResult(graphEditorMode.cleanupBeforeEntering(fallbackOptions));
this.graphEditorMode = graphEditorMode;
yield flowResult(this.graphEditorMode.initialize(fallbackOptions));
}
catch (error) {
assertErrorThrown(error);
this.applicationStore.notificationService.notifyWarning(`Can't enter text mode: transformation to grammar text failed. Error: ${error.message}`);
this.applicationStore.alertService.setBlockingAlert(undefined);
return;
}
break;
}
case GRAPH_EDITOR_MODE.FORM: {
if (this.graphState.checkIfApplicationUpdateOperationIsRunning()) {
return;
}
try {
try {
yield flowResult(this.graphEditorMode.onLeave(fallbackOptions));
this.graphEditorMode = new GraphEditFormModeState(this);
yield flowResult(this.graphEditorMode.initialize());
}
catch (error) {
yield flowResult(this.graphEditorMode.handleCleanupFailure(e