UNPKG

@finos/legend-application-studio

Version:
960 lines 51.1 kB
/** * 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