@finos/legend-studio
Version:
1,383 lines (1,311 loc) • 52.8 kB
text/typescript
/**
* 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, flowResult, makeAutoObservable } from 'mobx';
import { ClassEditorState } from './editor-state/element-editor-state/ClassEditorState.js';
import { ExplorerTreeState } from './ExplorerTreeState.js';
import {
ACTIVITY_MODE,
AUX_PANEL_MODE,
GRAPH_EDITOR_MODE,
EDITOR_MODE,
LEGEND_STUDIO_HOTKEY,
LEGEND_STUDIO_HOTKEY_MAP,
} from './EditorConfig.js';
import { ElementEditorState } from './editor-state/element-editor-state/ElementEditorState.js';
import { MappingEditorState } from './editor-state/element-editor-state/mapping/MappingEditorState.js';
import {
type GraphBuilderResult,
EditorGraphState,
GraphBuilderStatus,
} from './EditorGraphState.js';
import { ChangeDetectionState } from './ChangeDetectionState.js';
import { NewElementState } from './editor/NewElementState.js';
import { WorkspaceUpdaterState } from './sidebar-state/WorkspaceUpdaterState.js';
import { ProjectOverviewState } from './sidebar-state/ProjectOverviewState.js';
import { WorkspaceReviewState } from './sidebar-state/WorkspaceReviewState.js';
import { LocalChangesState } from './sidebar-state/LocalChangesState.js';
import { WorkspaceWorkflowManagerState } from './sidebar-state/WorkflowManagerState.js';
import { GrammarTextEditorState } from './editor-state/GrammarTextEditorState.js';
import {
type Clazz,
type GeneratorFn,
type PlainObject,
LogEvent,
addUniqueEntry,
isNonNullable,
assertErrorThrown,
guaranteeType,
guaranteeNonNullable,
UnsupportedOperationError,
assertNonNullable,
assertTrue,
ActionState,
filterByType,
} from '@finos/legend-shared';
import { UMLEditorState } from './editor-state/element-editor-state/UMLEditorState.js';
import { ServiceEditorState } from './editor-state/element-editor-state/service/ServiceEditorState.js';
import { EditorSDLCState } from './EditorSDLCState.js';
import { ModelLoaderState } from './editor-state/ModelLoaderState.js';
import type { EditorState } from './editor-state/EditorState.js';
import { EntityDiffViewState } from './editor-state/entity-diff-editor-state/EntityDiffViewState.js';
import { FunctionEditorState } from './editor-state/element-editor-state/FunctionEditorState.js';
import { ProjectConfigurationEditorState } from './editor-state/ProjectConfigurationEditorState.js';
import { PackageableRuntimeEditorState } from './editor-state/element-editor-state/RuntimeEditorState.js';
import { PackageableConnectionEditorState } from './editor-state/element-editor-state/connection/ConnectionEditorState.js';
import { PackageableDataEditorState } from './editor-state/element-editor-state/data/DataEditorState.js';
import { FileGenerationEditorState } from './editor-state/element-editor-state/FileGenerationEditorState.js';
import { EntityDiffEditorState } from './editor-state/entity-diff-editor-state/EntityDiffEditorState.js';
import { EntityChangeConflictEditorState } from './editor-state/entity-diff-editor-state/EntityChangeConflictEditorState.js';
import { CHANGE_DETECTION_EVENT } from './ChangeDetectionEvent.js';
import { GenerationSpecificationEditorState } from './editor-state/GenerationSpecificationEditorState.js';
import { UnsupportedElementEditorState } from './editor-state/UnsupportedElementEditorState.js';
import { FileGenerationViewerState } from './editor-state/FileGenerationViewerState.js';
import type { GenerationFile } from './shared/FileGenerationTreeUtil.js';
import type { ElementFileGenerationState } from './editor-state/element-editor-state/ElementFileGenerationState.js';
import { DevToolState } from './aux-panel-state/DevToolState.js';
import {
generateSetupRoute,
generateViewProjectRoute,
} from './LegendStudioRouter.js';
import {
HotkeyConfiguration,
NonBlockingDialogState,
PanelDisplayState,
} from '@finos/legend-art';
import type { DSL_LegendStudioApplicationPlugin_Extension } from './LegendStudioApplicationPlugin.js';
import type { Entity } from '@finos/legend-model-storage';
import {
ProjectConfiguration,
type SDLCServerClient,
type WorkspaceType,
} from '@finos/legend-server-sdlc';
import {
type PackageableElement,
type Type,
type Store,
type GraphManagerState,
GRAPH_MANAGER_EVENT,
PrimitiveType,
Class,
Enumeration,
Profile,
Association,
ConcreteFunctionDefinition,
Measure,
Database,
FlatData,
Mapping,
Service,
PackageableRuntime,
PackageableConnection,
FileGenerationSpecification,
GenerationSpecification,
PRIMITIVE_TYPE,
Package,
DataElement,
} from '@finos/legend-graph';
import type { DepotServerClient } from '@finos/legend-server-depot';
import type { LegendStudioPluginManager } from '../application/LegendStudioPluginManager.js';
import {
type ActionAlertInfo,
type BlockingAlertInfo,
ActionAlertActionType,
ActionAlertType,
APPLICATION_EVENT,
TAB_SIZE,
buildElementOption,
type PackageableElementOption,
} from '@finos/legend-application';
import { LEGEND_STUDIO_APP_EVENT } from './LegendStudioAppEvent.js';
import type { EditorMode } from './editor/EditorMode.js';
import { StandardEditorMode } from './editor/StandardEditorMode.js';
import { WorkspaceUpdateConflictResolutionState } from './sidebar-state/WorkspaceUpdateConflictResolutionState.js';
import {
graph_addElement,
graph_deleteElement,
graph_deleteOwnElement,
graph_renameElement,
} from './graphModifier/GraphModifierHelper.js';
import { PACKAGEABLE_ELEMENT_TYPE } from './shared/ModelUtil.js';
import { GlobalTestRunnerState } from './sidebar-state/testable/GlobalTestRunnerState.js';
import type { LegendStudioApplicationStore } from './LegendStudioBaseStore.js';
export abstract 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
*/
private readonly _$nominalTypeBrand!: 'EditorExtensionState';
}
export class EditorStore {
applicationStore: LegendStudioApplicationStore;
sdlcServerClient: SDLCServerClient;
depotServerClient: DepotServerClient;
pluginManager: LegendStudioPluginManager;
editorMode: EditorMode;
setEditorMode(val: EditorMode): void {
this.editorMode = val;
}
// 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;
setMode(val: EDITOR_MODE): void {
this.mode = val;
}
get isInViewerMode(): boolean {
return this.mode === EDITOR_MODE.VIEWER;
}
get isInConflictResolutionMode(): boolean {
return this.mode === EDITOR_MODE.CONFLICT_RESOLUTION;
}
editorExtensionStates: EditorExtensionState[] = [];
explorerTreeState: ExplorerTreeState;
sdlcState: EditorSDLCState;
graphState: EditorGraphState;
graphManagerState: GraphManagerState;
changeDetectionState: ChangeDetectionState;
grammarTextEditorState: GrammarTextEditorState;
modelLoaderState: ModelLoaderState;
projectConfigurationEditorState: ProjectConfigurationEditorState;
projectOverviewState: ProjectOverviewState;
workspaceWorkflowManagerState: WorkspaceWorkflowManagerState;
globalTestRunnerState: GlobalTestRunnerState;
workspaceUpdaterState: WorkspaceUpdaterState;
workspaceReviewState: WorkspaceReviewState;
localChangesState: LocalChangesState;
conflictResolutionState: WorkspaceUpdateConflictResolutionState;
devToolState: DevToolState;
private _isDisposed = false;
initState = ActionState.create();
graphEditMode = GRAPH_EDITOR_MODE.FORM;
// Aux Panel
activeAuxPanelMode: AUX_PANEL_MODE = AUX_PANEL_MODE.CONSOLE;
auxPanelDisplayState = new PanelDisplayState({
initial: 0,
default: 300,
snap: 100,
});
// Side Bar
activeActivity?: ACTIVITY_MODE = ACTIVITY_MODE.EXPLORER;
sideBarDisplayState = new PanelDisplayState({
initial: 300,
default: 300,
snap: 150,
});
// Hot keys
blockGlobalHotkeys = false;
defaultHotkeys: HotkeyConfiguration[] = [];
hotkeys: HotkeyConfiguration[] = [];
// Tabs
currentEditorState?: EditorState | undefined;
openedEditorStates: EditorState[] = [];
newElementState: 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: ElementFileGenerationState[] = [];
searchElementCommandState = new NonBlockingDialogState();
backdrop = false;
ignoreNavigationBlocking = false;
isDevToolEnabled = true;
constructor(
applicationStore: LegendStudioApplicationStore,
sdlcServerClient: SDLCServerClient,
depotServerClient: DepotServerClient,
graphManagerState: GraphManagerState,
pluginManager: LegendStudioPluginManager,
) {
makeAutoObservable(this, {
applicationStore: false,
sdlcServerClient: false,
depotServerClient: false,
graphState: false,
graphManagerState: false,
setEditorMode: action,
setMode: action,
setDevTool: action,
setHotkeys: action,
addHotKey: action,
resetHotkeys: action,
setBlockGlobalHotkeys: action,
setCurrentEditorState: action,
setBackdrop: action,
setActiveAuxPanelMode: action,
setIgnoreNavigationBlocking: action,
refreshCurrentEntityDiffEditorState: action,
setBlockingAlert: action,
setActionAlertInfo: action,
cleanUp: action,
reset: action,
setGraphEditMode: action,
setActiveActivity: action,
closeState: action,
closeAllOtherStates: action,
closeAllStates: action,
openState: action,
openEntityDiff: action,
openEntityChangeConflict: action,
openSingletonEditorState: action,
openElement: action,
reprocessElementEditorState: action,
openGeneratedFile: action,
closeAllEditorTabs: action,
});
this.applicationStore = applicationStore;
this.sdlcServerClient = sdlcServerClient;
this.depotServerClient = depotServerClient;
this.pluginManager = pluginManager;
this.editorMode = new StandardEditorMode(this);
this.sdlcState = new EditorSDLCState(this);
this.graphState = new EditorGraphState(this);
this.graphManagerState = graphManagerState;
this.changeDetectionState = new ChangeDetectionState(this, this.graphState);
this.devToolState = new DevToolState(this);
// side bar panels
this.explorerTreeState = new ExplorerTreeState(this);
this.projectOverviewState = new ProjectOverviewState(this, this.sdlcState);
this.globalTestRunnerState = new GlobalTestRunnerState(
this,
this.sdlcState,
);
this.workspaceWorkflowManagerState = new WorkspaceWorkflowManagerState(
this,
this.sdlcState,
);
this.workspaceUpdaterState = new WorkspaceUpdaterState(
this,
this.sdlcState,
);
this.workspaceReviewState = new WorkspaceReviewState(this, this.sdlcState);
this.localChangesState = new LocalChangesState(this, this.sdlcState);
this.conflictResolutionState = new WorkspaceUpdateConflictResolutionState(
this,
this.sdlcState,
);
this.newElementState = new NewElementState(this);
// special (singleton) editors
this.grammarTextEditorState = new GrammarTextEditorState(this);
this.modelLoaderState = new ModelLoaderState(this);
this.projectConfigurationEditorState = new ProjectConfigurationEditorState(
this,
this.sdlcState,
);
// extensions
this.editorExtensionStates = this.pluginManager
.getApplicationPlugins()
.flatMap(
(plugin) => plugin.getExtraEditorExtensionStateCreators?.() ?? [],
)
.map((creator) => creator(this))
.filter(isNonNullable);
// hotkeys
this.defaultHotkeys = [
// actions that need blocking
new HotkeyConfiguration(
LEGEND_STUDIO_HOTKEY.COMPILE,
[LEGEND_STUDIO_HOTKEY_MAP.COMPILE],
this.createGlobalHotKeyAction(() => {
flowResult(this.graphState.globalCompileInFormMode()).catch(
applicationStore.alertUnhandledError,
);
}),
),
new HotkeyConfiguration(
LEGEND_STUDIO_HOTKEY.GENERATE,
[LEGEND_STUDIO_HOTKEY_MAP.GENERATE],
this.createGlobalHotKeyAction(() => {
flowResult(
this.graphState.graphGenerationState.globalGenerate(),
).catch(applicationStore.alertUnhandledError);
}),
),
new HotkeyConfiguration(
LEGEND_STUDIO_HOTKEY.CREATE_ELEMENT,
[LEGEND_STUDIO_HOTKEY_MAP.CREATE_ELEMENT],
this.createGlobalHotKeyAction(() => this.newElementState.openModal()),
),
new HotkeyConfiguration(
LEGEND_STUDIO_HOTKEY.OPEN_ELEMENT,
[LEGEND_STUDIO_HOTKEY_MAP.OPEN_ELEMENT],
this.createGlobalHotKeyAction(() =>
this.searchElementCommandState.open(),
),
),
new HotkeyConfiguration(
LEGEND_STUDIO_HOTKEY.TOGGLE_TEXT_MODE,
[LEGEND_STUDIO_HOTKEY_MAP.TOGGLE_TEXT_MODE],
this.createGlobalHotKeyAction(() => {
flowResult(this.toggleTextMode()).catch(
applicationStore.alertUnhandledError,
);
}),
),
new HotkeyConfiguration(
LEGEND_STUDIO_HOTKEY.TOGGLE_MODEL_LOADER,
[LEGEND_STUDIO_HOTKEY_MAP.TOGGLE_MODEL_LOADER],
this.createGlobalHotKeyAction(() =>
this.openState(this.modelLoaderState),
),
),
new HotkeyConfiguration(
LEGEND_STUDIO_HOTKEY.SYNC_WITH_WORKSPACE,
[LEGEND_STUDIO_HOTKEY_MAP.SYNC_WITH_WORKSPACE],
this.createGlobalHotKeyAction(() => {
flowResult(this.localChangesState.pushLocalChanges()).catch(
applicationStore.alertUnhandledError,
);
}),
),
// simple actions (no blocking is needed)
new HotkeyConfiguration(
LEGEND_STUDIO_HOTKEY.TOGGLE_AUX_PANEL,
[LEGEND_STUDIO_HOTKEY_MAP.TOGGLE_AUX_PANEL],
this.createGlobalHotKeyAction(() => this.auxPanelDisplayState.toggle()),
),
new HotkeyConfiguration(
LEGEND_STUDIO_HOTKEY.TOGGLE_SIDEBAR_EXPLORER,
[LEGEND_STUDIO_HOTKEY_MAP.TOGGLE_SIDEBAR_EXPLORER],
this.createGlobalHotKeyAction(() =>
this.setActiveActivity(ACTIVITY_MODE.EXPLORER),
),
),
new HotkeyConfiguration(
LEGEND_STUDIO_HOTKEY.TOGGLE_SIDEBAR_CHANGES,
[LEGEND_STUDIO_HOTKEY_MAP.TOGGLE_SIDEBAR_CHANGES],
this.createGlobalHotKeyAction(() =>
this.setActiveActivity(ACTIVITY_MODE.LOCAL_CHANGES),
),
),
new HotkeyConfiguration(
LEGEND_STUDIO_HOTKEY.TOGGLE_SIDEBAR_WORKSPACE_REVIEW,
[LEGEND_STUDIO_HOTKEY_MAP.TOGGLE_SIDEBAR_WORKSPACE_REVIEW],
this.createGlobalHotKeyAction(() =>
this.setActiveActivity(ACTIVITY_MODE.WORKSPACE_REVIEW),
),
),
new HotkeyConfiguration(
LEGEND_STUDIO_HOTKEY.TOGGLE_SIDEBAR_WORKSPACE_UPDATER,
[LEGEND_STUDIO_HOTKEY_MAP.TOGGLE_SIDEBAR_WORKSPACE_UPDATER],
this.createGlobalHotKeyAction(() =>
this.setActiveActivity(ACTIVITY_MODE.WORKSPACE_UPDATER),
),
),
];
this.hotkeys = this.defaultHotkeys;
}
get isInitialized(): boolean {
return (
Boolean(
this.sdlcState.currentProject &&
this.sdlcState.currentWorkspace &&
this.sdlcState.currentRevision &&
this.sdlcState.remoteWorkspaceRevision,
) && this.graphManagerState.systemBuildState.hasSucceeded
);
}
get isInGrammarTextMode(): boolean {
return this.graphEditMode === GRAPH_EDITOR_MODE.GRAMMAR_TEXT;
}
get isInFormMode(): boolean {
return this.graphEditMode === GRAPH_EDITOR_MODE.FORM;
}
get hasUnpushedChanges(): boolean {
return Boolean(
this.changeDetectionState.workspaceLocalLatestRevisionState.changes
.length,
);
}
setDevTool(val: boolean): void {
this.isDevToolEnabled = val;
}
setHotkeys(val: HotkeyConfiguration[]): void {
this.hotkeys = val;
}
addHotKey(val: HotkeyConfiguration): void {
addUniqueEntry(this.hotkeys, val);
}
resetHotkeys(): void {
this.hotkeys = this.defaultHotkeys;
}
setBlockGlobalHotkeys(val: boolean): void {
this.blockGlobalHotkeys = val;
}
setCurrentEditorState(val: EditorState | undefined): void {
this.currentEditorState = val;
}
setBackdrop(val: boolean): void {
this.backdrop = val;
}
setActiveAuxPanelMode(val: AUX_PANEL_MODE): void {
this.activeAuxPanelMode = val;
}
setIgnoreNavigationBlocking(val: boolean): void {
this.ignoreNavigationBlocking = val;
}
refreshCurrentEntityDiffEditorState(): void {
if (this.currentEditorState instanceof EntityDiffEditorState) {
this.currentEditorState.refresh();
}
}
setBlockingAlert(alertInfo: BlockingAlertInfo | undefined): void {
if (this._isDisposed) {
return;
}
this.setBlockGlobalHotkeys(Boolean(alertInfo)); // block global hotkeys if alert is shown
this.applicationStore.setBlockingAlert(alertInfo);
}
setActionAlertInfo(alertInfo: ActionAlertInfo | undefined): void {
if (this._isDisposed) {
return;
}
this.applicationStore.setActionAlertInfo(alertInfo);
}
cleanUp(): void {
// 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.setBlockingAlert(undefined);
this.setActionAlertInfo(undefined);
// stop change detection to avoid memory-leak
this.changeDetectionState.stop();
this._isDisposed = true;
}
reset(): void {
this.closeAllEditorTabs();
this.projectConfigurationEditorState = new ProjectConfigurationEditorState(
this,
this.sdlcState,
);
this.explorerTreeState = new ExplorerTreeState(this);
}
/**
* 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: string,
workspaceId: string,
workspaceType: WorkspaceType,
): GeneratorFn<void> {
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.log.info(
LogEvent.create(APPLICATION_EVENT.DEVELOPMENT_ISSUE),
`Fast-refreshing the app - undoing cleanUp() and preventing initialize() recall in editor store...`,
);
this.changeDetectionState.start();
this._isDisposed = false;
return;
}
this.applicationStore.notifyIllegalState(
'Editor store is re-initialized',
);
return;
}
this.initState.inProgress();
const onLeave = (hasBuildSucceeded: boolean): void => {
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.setActionAlertInfo({
message: `Project not found or inaccessible`,
prompt: 'Please check that the project exists and request access to it',
type: ActionAlertType.STANDARD,
onEnter: (): void => this.setBlockGlobalHotkeys(true),
onClose: (): void => this.setBlockGlobalHotkeys(false),
actions: [
{
label: 'Reload application',
default: true,
type: ActionAlertActionType.STANDARD,
handler: (): void => {
this.applicationStore.navigator.reload();
},
},
{
label: 'Back to setup page',
type: ActionAlertActionType.STANDARD,
handler: (): void => {
this.applicationStore.navigator.goTo(
generateSetupRoute(undefined),
);
},
},
],
});
onLeave(false);
return;
}
yield flowResult(
this.sdlcState.fetchCurrentWorkspace(
projectId,
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 (): Promise<void> => {
try {
this.applicationStore.setBlockingAlert({
message: 'Creating workspace...',
prompt: 'Please do not close the application',
});
const workspace = await this.sdlcServerClient.createWorkspace(
projectId,
workspaceId,
workspaceType,
);
this.applicationStore.setBlockingAlert(undefined);
this.applicationStore.notifySuccess(
`Workspace '${workspace.workspaceId}' is succesfully created. Reloading application...`,
);
this.applicationStore.navigator.reload();
} catch (error) {
assertErrorThrown(error);
this.applicationStore.log.error(
LogEvent.create(LEGEND_STUDIO_APP_EVENT.WORKSPACE_SETUP_FAILURE),
error,
);
this.applicationStore.notifyError(error);
}
};
this.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,
onEnter: (): void => this.setBlockGlobalHotkeys(true),
onClose: (): void => this.setBlockGlobalHotkeys(false),
actions: [
{
label: 'View project',
default: true,
type: ActionAlertActionType.STANDARD,
handler: (): void => {
this.applicationStore.navigator.goTo(
generateViewProjectRoute(projectId),
);
},
},
{
label: 'Create workspace',
type: ActionAlertActionType.STANDARD,
handler: (): void => {
createWorkspaceAndRelaunch().catch(
this.applicationStore.alertUnhandledError,
);
},
},
{
label: 'Back to setup page',
type: ActionAlertActionType.STANDARD,
handler: (): void => {
this.applicationStore.navigator.goTo(
generateSetupRoute(projectId, workspaceId, workspaceType),
);
},
},
],
});
onLeave(false);
return;
}
yield Promise.all([
this.sdlcState.fetchCurrentRevision(
projectId,
this.sdlcState.activeWorkspace,
),
this.graphManagerState.initializeSystem(), // this can be moved inside of `setupEngine`
this.graphManagerState.graphManager.initialize(
{
env: this.applicationStore.config.env,
tabSize: TAB_SIZE,
clientConfig: {
baseUrl: this.applicationStore.config.engineServerUrl,
queryBaseUrl: this.applicationStore.config.engineQueryServerUrl,
enableCompression: true,
},
},
{
tracerService: this.applicationStore.tracerService,
},
),
]);
yield flowResult(this.initMode());
onLeave(true);
}
*initMode(): GeneratorFn<void> {
switch (this.mode) {
case EDITOR_MODE.STANDARD:
yield flowResult(this.initStandardMode());
return;
case EDITOR_MODE.CONFLICT_RESOLUTION:
yield flowResult(this.initConflictResolutionMode());
return;
default:
throw new UnsupportedOperationError(
`Can't initialize editor for unsupported mode '${this.mode}'`,
);
}
}
private *initStandardMode(): GeneratorFn<void> {
yield Promise.all([
this.buildGraph(),
this.sdlcState.checkIfWorkspaceIsOutdated(),
this.workspaceReviewState.fetchCurrentWorkspaceReview(),
this.workspaceUpdaterState.fetchLatestCommittedReviews(),
this.projectConfigurationEditorState.fetchLatestProjectStructureVersion(),
this.graphState.graphGenerationState.fetchAvailableFileGenerationDescriptions(),
this.graphState.graphGenerationState.externalFormatState.fetchExternalFormatsDescriptions(),
this.modelLoaderState.fetchAvailableModelImportDescriptions(),
this.sdlcState.fetchProjectVersions(),
]);
}
private *initConflictResolutionMode(): GeneratorFn<void> {
this.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,
onEnter: (): void => this.setBlockGlobalHotkeys(true),
onClose: (): void => this.setBlockGlobalHotkeys(false),
actions: [
{
label: 'Discard your changes',
type: ActionAlertActionType.PROCEED_WITH_CAUTION,
handler: (): void => {
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.fetchAvailableFileGenerationDescriptions(),
this.graphState.graphGenerationState.externalFormatState.fetchExternalFormatsDescriptions(),
this.modelLoaderState.fetchAvailableModelImportDescriptions(),
this.sdlcState.fetchProjectVersions(),
]);
}
*buildGraph(graphEntities?: Entity[]): GeneratorFn<void> {
const startTime = Date.now();
let entities: Entity[];
let projectConfiguration: PlainObject<ProjectConfiguration>;
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;
const result = (yield Promise.all([
this.sdlcServerClient.getEntities(projectId, activeWorkspace),
this.sdlcServerClient.getConfiguration(projectId, activeWorkspace),
])) as [Entity[], PlainObject<ProjectConfiguration>];
entities = result[0];
projectConfiguration = result[1];
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),
);
this.changeDetectionState.workspaceLocalLatestRevisionState.setEntities(
entities,
);
this.applicationStore.log.info(
LogEvent.create(GRAPH_MANAGER_EVENT.GRAPH_ENTITIES_FETCHED),
Date.now() - startTime,
'ms',
);
} catch {
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),
)) as GraphBuilderResult;
if (result.error) {
if (result.status === GraphBuilderStatus.REDIRECTED_TO_TEXT_MODE) {
yield flowResult(
this.changeDetectionState.workspaceLocalLatestRevisionState.buildEntityHashesIndex(
entities,
LogEvent.create(
CHANGE_DETECTION_EVENT.CHANGE_DETECTION_LOCAL_HASHES_INDEX_BUILT,
),
),
);
}
return;
}
// build explorer tree
this.explorerTreeState.buildImmutableModelTrees();
this.explorerTreeState.build();
// ======= (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(
CHANGE_DETECTION_EVENT.CHANGE_DETECTION_LOCAL_HASHES_INDEX_BUILT,
),
),
this.sdlcState.buildWorkspaceBaseRevisionEntityHashesIndex(),
this.sdlcState.buildProjectLatestRevisionEntityHashesIndex(),
]);
this.changeDetectionState.start();
yield Promise.all([
this.changeDetectionState.computeAggregatedWorkspaceChanges(true),
this.changeDetectionState.computeAggregatedProjectLatestChanges(true),
]);
this.applicationStore.log.info(
LogEvent.create(CHANGE_DETECTION_EVENT.CHANGE_DETECTION_RESTARTED),
'[ASNYC]',
);
// ======= FINISHED (RE)START CHANGE DETECTION =======
} catch (error) {
assertErrorThrown(error);
this.applicationStore.log.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;
}
}
getCurrentEditorState<T extends EditorState>(clazz: Clazz<T>): T {
return guaranteeType(
this.currentEditorState,
clazz,
`Current editor state is not of the specified type (this is likely caused by calling this method at the wrong place)`,
);
}
getEditorExtensionState<T extends EditorExtensionState>(clazz: Clazz<T>): T {
return guaranteeNonNullable(
this.editorExtensionStates.find(filterByType(clazz)),
`Can't find extension editor state of the specified type: no built extension editor state available from plugins`,
);
}
setGraphEditMode(graphEditor: GRAPH_EDITOR_MODE): void {
this.graphEditMode = graphEditor;
this.graphState.clearCompilationError();
}
setActiveActivity(
activity: ACTIVITY_MODE,
options?: { keepShowingIfMatchedCurrent?: boolean },
): void {
if (!this.sideBarDisplayState.isOpen) {
this.sideBarDisplayState.open();
} else if (
activity === this.activeActivity &&
!options?.keepShowingIfMatchedCurrent
) {
this.sideBarDisplayState.close();
}
this.activeActivity = activity;
}
closeState(editorState: EditorState): void {
const elementIndex = this.openedEditorStates.findIndex(
(e) => e === editorState,
);
assertTrue(elementIndex !== -1, `Can't close a tab which is not opened`);
this.openedEditorStates.splice(elementIndex, 1);
if (this.currentEditorState === editorState) {
if (this.openedEditorStates.length) {
const openIndex = elementIndex - 1;
this.setCurrentEditorState(
openIndex >= 0
? this.openedEditorStates[openIndex]
: this.openedEditorStates[0],
);
} else {
this.setCurrentEditorState(undefined);
}
}
this.explorerTreeState.reprocess();
}
closeAllOtherStates(editorState: EditorState): void {
assertNonNullable(
this.openedEditorStates.find((e) => e === editorState),
'Editor tab should be currently opened',
);
this.currentEditorState = editorState;
this.openedEditorStates = [editorState];
this.explorerTreeState.reprocess();
}
closeAllStates(): void {
this.closeAllEditorTabs();
this.explorerTreeState.reprocess();
}
openState(editorState: EditorState): void {
if (editorState instanceof ElementEditorState) {
this.openElement(editorState.element);
} else if (editorState instanceof EntityDiffViewState) {
this.openEntityDiff(editorState);
} else if (editorState instanceof EntityChangeConflictEditorState) {
this.openEntityChangeConflict(editorState);
} else if (editorState instanceof FileGenerationViewerState) {
this.openGeneratedFile(editorState.generatedFile);
} else if (editorState === this.modelLoaderState) {
this.openSingletonEditorState(this.modelLoaderState);
} else if (editorState === this.projectConfigurationEditorState) {
this.openSingletonEditorState(this.projectConfigurationEditorState);
} else {
throw new UnsupportedOperationError(
`Can't open editor state`,
editorState,
);
}
this.explorerTreeState.reprocess();
}
openEntityDiff(entityDiffEditorState: EntityDiffViewState): void {
const existingEditorState = this.openedEditorStates.find(
(editorState) =>
editorState instanceof EntityDiffViewState &&
editorState.fromEntityPath === entityDiffEditorState.fromEntityPath &&
editorState.toEntityPath === entityDiffEditorState.toEntityPath &&
editorState.fromRevision === entityDiffEditorState.fromRevision &&
editorState.toRevision === entityDiffEditorState.toRevision,
);
const diffEditorState = existingEditorState ?? entityDiffEditorState;
if (!existingEditorState) {
this.openedEditorStates.push(diffEditorState);
}
this.setCurrentEditorState(diffEditorState);
}
openEntityChangeConflict(
entityChangeConflictEditorState: EntityChangeConflictEditorState,
): void {
const existingEditorState = this.openedEditorStates.find(
(editorState) =>
editorState instanceof EntityChangeConflictEditorState &&
editorState.entityPath === entityChangeConflictEditorState.entityPath,
);
const conflictEditorState =
existingEditorState ?? entityChangeConflictEditorState;
if (!existingEditorState) {
this.openedEditorStates.push(conflictEditorState);
}
this.setCurrentEditorState(conflictEditorState);
}
/**
* This method helps open editor that only exists one instance at at time such as model-loader, project config, settings ...
*/
openSingletonEditorState(
singularEditorState: ModelLoaderState | ProjectConfigurationEditorState,
): void {
const existingEditorState = this.openedEditorStates.find(
(e) => e === singularEditorState,
);
const editorState = existingEditorState ?? singularEditorState;
if (!existingEditorState) {
this.openedEditorStates.push(editorState);
}
this.setCurrentEditorState(editorState);
}
createElementState(
element: PackageableElement,
): ElementEditorState | undefined {
if (element instanceof PrimitiveType) {
throw new UnsupportedOperationError(
`Can't create element state for primitive type`,
);
} else if (element instanceof Class) {
return new ClassEditorState(this, element);
} else if (
element instanceof Association ||
element instanceof Enumeration ||
element instanceof Profile
) {
return new UMLEditorState(this, element);
} else if (element instanceof ConcreteFunctionDefinition) {
return new FunctionEditorState(this, element);
} else if (
element instanceof Measure ||
element instanceof Database ||
element instanceof FlatData
) {
return new UnsupportedElementEditorState(this, element);
} else if (element instanceof PackageableRuntime) {
return new PackageableRuntimeEditorState(this, element);
} else if (element instanceof PackageableConnection) {
return new PackageableConnectionEditorState(this, element);
} else if (element instanceof Mapping) {
return new MappingEditorState(this, element);
} else if (element instanceof Service) {
return new ServiceEditorState(this, element);
} else if (element instanceof GenerationSpecification) {
return new GenerationSpecificationEditorState(this, element);
} else if (element instanceof FileGenerationSpecification) {
return new FileGenerationEditorState(this, element);
} else if (element instanceof DataElement) {
return new PackageableDataEditorState(this, element);
}
const extraElementEditorStateCreators = this.pluginManager
.getApplicationPlugins()
.flatMap(
(plugin) =>
(
plugin as DSL_LegendStudioApplicationPlugin_Extension
).getExtraElementEditorStateCreators?.() ?? [],
);
for (const creator of extraElementEditorStateCreators) {
const elementEditorState = creator(this, element);
if (elementEditorState) {
return elementEditorState;
}
}
throw new UnsupportedOperationError(
`Can't create editor state for element: no compatible editor state creator available from plugins`,
element,
);
}
openElement(element: PackageableElement): void {
if (this.isInGrammarTextMode) {
// in text mode, we want to select the block of code that corresponds to the element if possible
// the cheap way to do this is to search by element label text, e.g. `Mapping some::package::someMapping`
this.grammarTextEditorState.setCurrentElementLabelRegexString(element);
} else {
if (!(element instanceof Package)) {
const existingElementState = this.openedEditorStates.find(
(state) =>
state instanceof ElementEditorState && state.element === element,
);
const elementState =
existingElementState ?? this.createElementState(element);
if (elementState && !existingElementState) {
this.openedEditorStates.push(elementState);
}
this.setCurrentEditorState(elementState);
}
// expand tree node
this.explorerTreeState.openNode(element);
}
}
*addElement(
element: PackageableElement,
packagePath: string | undefined,
openAfterCreate: boolean,
): GeneratorFn<void> {
graph_addElement(
this.graphManagerState.graph,
element,
packagePath,
this.changeDetectionState.observerContext,
);
this.explorerTreeState.reprocess();
if (openAfterCreate) {
this.openElement(element);
}
}
*deleteElement(element: PackageableElement): GeneratorFn<void> {
if (
this.graphState.checkIfApplicationUpdateOperationIsRunning() ||
this.graphManagerState.isElementReadOnly(element)
) {
return;
}
const generatedChildrenElements = (
this.graphState.graphGenerationState.generatedEntities.get(
element.path,
) ?? []
)
.map((genChildEntity) =>
this.graphManagerState.graph.generationModel.allOwnElements.find(
(genElement) => genElement.path === genChildEntity.path,
),
)
.filter(isNonNullable);
const elementsToDelete = [element, ...generatedChildrenElements];
this.openedEditorStates = this.openedEditorStates.filter((elementState) => {
if (elementState instanceof ElementEditorState) {
if (elementState === this.currentEditorState) {
// avoid closing the current editor state as this will be taken care of
// by the `closeState()` call later
return true;
}
return !generatedChildrenElements.includes(elementState.element);
}
return true;
});
if (
this.currentEditorState &&
this.currentEditorState instanceof ElementEditorState &&
elementsToDelete.includes(this.currentEditorState.element)
) {
this.closeState(this.currentEditorState);
}
// remove/retire the element's generated children before remove the element itself
generatedChildrenElements.forEach((el) =>
graph_deleteOwnElement(this.graphManagerState.graph.generationModel, el),
);
graph_deleteElement(this.graphManagerState.graph, element);
const extraElementEditorPostDeleteActions = this.pluginManager
.getApplicationPlugins()
.flatMap(
(plugin) =>
(
plugin as DSL_LegendStudioApplicationPlugin_Extension
).getExtraElementEditorPostDeleteActions?.() ?? [],
);
for (const postDeleteAction of extraElementEditorPostDeleteActions) {
postDeleteAction(this, element);
}
// reprocess project explorer tree
this.explorerTreeState.reprocess();
// recompile
yield flowResult(
this.graphState.globalCompileInFormMode({
message: `Can't compile graph after deletion and error cannot be located in form mode. Redirected to text mode for debugging`,
}),
);
}
*renameElement(
element: PackageableElement,
newPath: string,
): GeneratorFn<void> {
if (this.graphManagerState.isElementReadOnly(element)) {
return;
}
graph_renameElement(
this.graphManagerState.graph,
element,
newPath,
this.changeDetectionState.observerContext,
);
const extraElementEditorPostRenameActions = this.pluginManager
.getApplicationPlugins()
.flatMap(
(plugin) =>
(
plugin as DSL_LegendStudioApplicationPlugin_Extension
).getExtraElementEditorPostRenameActions?.() ?? [],
);
for (const postRenameAction of extraElementEditorPostRenameActions) {
postRenameAction(this, element);
}
// reprocess project explorer tree
this.explorerTreeState.reprocess();
if (element instanceof Package) {
this.explorerTreeState.openNode(element);
} else if (element.package) {
this.explorerTreeState.openNode(element.package);
}
// recompile
yield flowResult(
this.graphState.globalCompileInFormMode({
message: `Can't compile graph after renaming and error cannot be located in form mode. Redirected to text mode for debugging`,
}),
);
}
// TODO: to be removed when we process editor states properly
reprocessElementEditorState = (
editorState: EditorState,
): EditorState | undefined => {
if (editorState instanceof ElementEditorState) {
const correspondingElement =
this.graphManagerState.graph.getNullableElement(
editorState.element.path,
);
if (correspondingElement) {
return editorState.reprocess(correspondingElement, this);
}
}
// No need to reprocess generated file state as it has no reference to any of the graphs
if (editorState instanceof FileGenerationViewerState) {
return editorState;
}
return undefined;
};
// TODO: to be removed when we process editor states properly
findCurrentEditorState = (
editor: EditorState | undefined,
): EditorState | undefined => {
if (editor instanceof ElementEditorState) {
return this.openedEditorStates.find(
(es): es is ElementEditorState =>
es instanceof ElementEditorState &&
es.element.path === editor.element.path,
);
}
if (editor instanceof FileGenerationViewerState) {
return this.openedEditorStates.find((e) => e === editor);
}
return undefined;
};
openGeneratedFile(file: GenerationFile): void {
const existingGeneratedFileState = this.openedEditorStates.find(
(editorState) =>
editorState instanceof FileGenerationViewerState &&
editorState.generatedFile === file,
);
const generatedFileState =
existingGeneratedFileState ?? new FileGenerationViewerState(this, file);
if (!existingGeneratedFileState) {
this.openedEditorStates.push(generatedFileState);
}
this.setCurrentEditorState(generatedFileState);
}
createGlobalHotKeyAction =
(
handler: (event?: KeyboardEvent) => void,
preventDefault = true,
): ((event?: KeyboardEvent) => void) =>
(event?: KeyboardEvent): void => {
if (preventDefault) {
event?.preventDefault();
}
// TODO: maybe we should come up with a better way to block global hot keys, this seems highly restrictive.
const isResolvingConflicts =
this.isInConflictResolutionMode &&
!this.conflictResolutionState.hasResolvedAllConflicts;
if (
(this.isInitialized &&
!isResolvingConflicts &&
!this.blockGlobalHotkeys) ||
this.isInViewerMode
) {
handler(event);
}
};
closeAllEditorTabs(): void {
this.setCurrentEditorState(undefined);
this.openedEditorStates = [];
}
*toggleTextMode(): GeneratorFn<void> {
if (this.isInFormMode) {
if (this.graphState.checkIfApplicationUpdateOperationIsRunning()) {
return;
}
this.setBlockingAlert({
message: 'Switching to text mode...',
showLoading: true,
});
try {
const graphGrammar =
(yield this.graphManagerState.graphManager.graphToPureCode(
this.graphManagerState.graph,
)) as string;
yield flowResult(
this.grammarTextEditorState.setGraphGrammarText(graphGrammar),
);
} catch (error) {
assertErrorThrown(error);
this.applicationStore.notifyWarning(
`Can't enter text mode: transformation to grammar text failed. Error: ${error.message}`,
);
this.setBlockingAlert(undefined);
return;
}
this.setBlockingAlert(undefined);
this.setGraphEditMode(GRAPH_EDITOR_MODE.GRAMMAR_TEXT);
// navigate to the currently opened element immediately after entering text mode editor
if (this.currentEditorState instanceof ElementEditorState) {
this.grammarTextEditorState.setCurrentElementLabelRegexString(
this.currentEditorState.element,
);
}
} else if (this.isInGrammarTextMode) {
yield flowResult(this.graphState.leaveTextMode());
} else {
throw new UnsupportedOperationError(
'Editor only support form mode and text mode at the moment',
);
}
}
get enumerationOptions(): PackageableElementOption<Enumeration>[] {
return this.graphManagerState.graph.ownEnumerations
.concat(this.graphManagerState.graph.dependencyManager.enumerations)
.map(buildElementOption);
}
get classOptions(): PackageableElementOption<Class>[] {
return this.graphManagerState.graph.ownClasses
.concat(
this.graphManagerState.filterSystemElementOptions(
this.graphManagerState.graph.systemModel.ownClasses,
),
)
.concat(this.graphManagerState.graph.dependencyManager.classes)
.map(buildElementOption);
}
get associationOptions(): PackageableElementOption<Association>[] {
return this.graphManagerState.graph.ownAssociations
.concat(
this.graphManagerState.filterSystemElementOptions(
this.graphManagerState.graph.systemModel.ownAssociations,
),
)
.concat(this.graphManagerState.graph.dependencyManager.associations)
.map(buildElementOption);
}
get profileOptions(): PackageableElementOption<Profile>[] {
return this.graphManagerState.graph.ownProfiles
.concat(
this.graphManagerState.filterSystemElementOptions(
this.graphManagerState.graph.systemModel.ownProfiles,
),
)
.concat(this.graphManagerState.graph.dependencyManager.prof