UNPKG

@finos/legend-application-studio

Version:
471 lines 27 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, flowResult, makeObservable, observable, flow } from 'mobx'; import { LEGEND_STUDIO_APP_EVENT } from '../../../__lib__/LegendStudioEvent.js'; import { LogEvent, assertErrorThrown, assertTrue, guaranteeNonNullable, isNonNullable, NetworkClientError, HttpStatus, hashObject, deleteEntry, } from '@finos/legend-shared'; import { EntityDiffViewState } from '../editor-state/entity-diff-editor-state/EntityDiffViewState.js'; import { SPECIAL_REVISION_ALIAS } from '../editor-state/entity-diff-editor-state/EntityDiffEditorState.js'; import { EntityChangeConflictEditorState } from '../editor-state/entity-diff-editor-state/EntityChangeConflictEditorState.js'; import { ACTIVITY_MODE } from '../EditorConfig.js'; import { EntityChangeType, EntityDiff, ProjectConfiguration, Revision, RevisionAlias, } from '@finos/legend-server-sdlc'; import { AbstractConflictResolutionState } from '../AbstractConflictResolutionState.js'; export class WorkspaceUpdateConflictResolutionState extends AbstractConflictResolutionState { isInitializingConflictResolution = false; hasResolvedAllConflicts = false; isAcceptingConflictResolution = false; isDiscardingConflictResolutionChanges = false; isAbortingConflictResolution = false; constructor(editorStore, sdlcState) { super(editorStore, sdlcState); makeObservable(this, { editorStore: false, sdlcState: false, mergeEditorStates: observable, isInitializingConflictResolution: observable, isAcceptingConflictResolution: observable, isDiscardingConflictResolutionChanges: observable, isAbortingConflictResolution: observable, hasResolvedAllConflicts: observable, openConflict: action, closeConflict: action, resolveConflict: action, markConflictAsResolved: flow, initialize: flow, buildGraphInConflictResolutionMode: flow, buildConflictResolutionLatestRevisionEntityHashesIndex: flow, initProjectConfigurationInConflictResolutionMode: flow, acceptConflictResolution: flow, buildConflictResolutionBaseRevisionEntityHashesIndex: flow, initChangeDetectionInConflictResolutionMode: flow, discardConflictResolutionChanges: flow, abortConflictResolution: flow, promptBuildGraphAfterAllConflictsResolved: flow, confirmHasResolvedAllConflicts: action, openConflictResolutionChange: action, }); } get resolutions() { return this.editorStore.changeDetectionState.resolutions; } get conflicts() { return this.editorStore.changeDetectionState.conflicts.filter((conflict) => !this.resolutions .map((resolution) => resolution.entityPath) .includes(conflict.entityPath)); } get resolvedChanges() { return this.resolutions .map((resolution) => { const path = resolution.entityPath; const fromEntity = this.editorStore.changeDetectionState.conflictResolutionBaseRevisionState.entities.find((e) => e.path === path); const toEntity = resolution.resolvedEntity; if (!fromEntity && !toEntity) { return undefined; } else if (!fromEntity) { return new EntityDiff(undefined, path, EntityChangeType.CREATE); } else if (!toEntity) { return new EntityDiff(path, undefined, EntityChangeType.DELETE); } return hashObject(toEntity.content) === hashObject(fromEntity.content) ? undefined : new EntityDiff(path, path, EntityChangeType.MODIFY); }) .filter(isNonNullable); } get changes() { return this.hasResolvedAllConflicts ? this.editorStore.changeDetectionState .conflictResolutionBaseRevisionState.changes : this.editorStore.changeDetectionState.aggregatedConflictResolutionChanges .filter((change) => !this.conflicts .map((conflict) => conflict.entityPath) .includes(change.entityPath)) .filter((change) => !this.resolutions .map((resolution) => resolution.entityPath) .includes(change.entityPath)) .filter((change) => !this.resolvedChanges .map((resolvedChange) => resolvedChange.entityPath) .includes(change.entityPath)) .concat(this.resolvedChanges); } openConflict(conflict) { const existingMergeEditorState = this.mergeEditorStates.find((state) => state.entityPath === conflict.entityPath); if (existingMergeEditorState) { this.editorStore.tabManagerState.openTab(existingMergeEditorState); return; } const baseEntityGetter = (entityPath) => entityPath ? this.editorStore.changeDetectionState.workspaceBaseRevisionState.entities.find((e) => e.path === entityPath) : undefined; const currentChangeEntityGetter = (entityPath) => entityPath ? this.editorStore.changeDetectionState.workspaceLocalLatestRevisionState.entities.find((e) => e.path === entityPath) : undefined; const incomingChangeEntityGetter = (entityPath) => entityPath ? this.editorStore.changeDetectionState.conflictResolutionBaseRevisionState.entities.find((e) => e.path === entityPath) : undefined; const mergeEditorState = new EntityChangeConflictEditorState(this.editorStore, this, conflict.entityPath, SPECIAL_REVISION_ALIAS.WORKSPACE_BASE, SPECIAL_REVISION_ALIAS.WORKSPACE_HEAD, SPECIAL_REVISION_ALIAS.WORKSPACE_UPDATE, baseEntityGetter(conflict.entityPath), currentChangeEntityGetter(conflict.entityPath), incomingChangeEntityGetter(conflict.entityPath), baseEntityGetter, currentChangeEntityGetter, incomingChangeEntityGetter); this.mergeEditorStates.push(mergeEditorState); this.editorStore.tabManagerState.openTab(mergeEditorState); } closeConflict(conflict) { this.editorStore.tabManagerState.closeTab(conflict); } resolveConflict(resolution) { this.editorStore.changeDetectionState.resolutions.push(resolution); } *markConflictAsResolved(conflictState) { // swap out the current conflict editor with a normal diff editor const resolvedChange = this.resolvedChanges.find((change) => change.entityPath === conflictState.entityPath); if (resolvedChange) { this.openConflictResolutionChange(resolvedChange); } this.closeConflict(conflictState); deleteEntry(this.mergeEditorStates, conflictState); // check for remaining conflicts, if none left, prompt the users for the next action yield flowResult(this.promptBuildGraphAfterAllConflictsResolved()); } confirmHasResolvedAllConflicts() { this.hasResolvedAllConflicts = true; this.mergeEditorStates = []; // make sure we clean this to avoid any potential memory-leak } *initProjectConfigurationInConflictResolutionMode() { assertTrue(this.editorStore.isInConflictResolutionMode, 'Editor must be in conflict resolution mode to call this method'); const projectConfiguration = (yield this.editorStore.sdlcServerClient.getConfigurationOfWorkspaceInConflictResolutionMode(this.sdlcState.activeProject.projectId, this.sdlcState.activeWorkspace)); this.editorStore.projectConfigurationEditorState.setProjectConfiguration(ProjectConfiguration.serialization.fromJson(projectConfiguration)); // make sure we set the original project configuration to a different object this.editorStore.projectConfigurationEditorState.setOriginalProjectConfiguration(ProjectConfiguration.serialization.fromJson(projectConfiguration)); } *initChangeDetectionInConflictResolutionMode() { try { const startTime = Date.now(); // ======= (RE)START CHANGE DETECTION ======= this.editorStore.changeDetectionState.stop(); yield Promise.all([ this.sdlcState.buildWorkspaceBaseRevisionEntityHashesIndex(), this.sdlcState.buildWorkspaceLatestRevisionEntityHashesIndex(), this.buildConflictResolutionBaseRevisionEntityHashesIndex(), this.buildConflictResolutionLatestRevisionEntityHashesIndex(), ]); this.editorStore.changeDetectionState.start(); yield Promise.all([ this.editorStore.changeDetectionState.computeAggregatedWorkspaceChanges(true), this.editorStore.changeDetectionState.computeAggregatedConflictResolutionChanges(true), ]); this.editorStore.applicationStore.logService.info(LogEvent.create(LEGEND_STUDIO_APP_EVENT.CHANGE_DETECTION_RESTART__SUCCESS), Date.now() - startTime, 'ms'); // ======= FINISHED (RE)START CHANGE DETECTION ======= } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.logService.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.SDLC_MANAGER_FAILURE), error); this.editorStore.applicationStore.notificationService.notifyError(error); this.sdlcState.handleChangeDetectionRefreshIssue(error); throw error; } } *initialize() { assertTrue(this.editorStore.isInConflictResolutionMode, 'Editor must be in conflict resolution mode to call this method'); try { this.isInitializingConflictResolution = true; yield flowResult(this.initChangeDetectionInConflictResolutionMode()); /** * NOTE: There is a weird case where conflict resolution operations failed to delete the conflict resolution * workspace, so the user after accepting conflict resolution, ending up in conflict resolution mode * again without any conflicts, here we want to immediately redirect them to explorer panel to prompt * them to build graph */ if (!this.conflicts.length) { this.editorStore.setActiveActivity(ACTIVITY_MODE.EXPLORER); } } finally { this.isInitializingConflictResolution = false; } } *buildGraphInConflictResolutionMode() { assertTrue(this.editorStore.isInConflictResolutionMode && this.hasResolvedAllConflicts, 'Editor must be in conflict resolution mode and all conflicts must have been marked as resolved to call this method'); this.editorStore.tabManagerState.closeAllTabs(); this.editorStore.setActiveActivity(ACTIVITY_MODE.EXPLORER, { keepShowingIfMatchedCurrent: true, }); try { this.editorStore.changeDetectionState.stop(); // stop change detection (because it is already running) so we can build the graph // NOTE: here we patch conflict resolution workspace HEAD entities with the entities from resolved conflicts to build graph with those const workspaceLatestEntities = this.editorStore.changeDetectionState .conflictResolutionHeadRevisionState.entities; const entities = workspaceLatestEntities .filter((entity) => !this.resolutions .map((resolution) => resolution.entityPath) .includes(entity.path)) .concat(this.resolutions .map((resolution) => resolution.resolvedEntity) .filter(isNonNullable)); // build graph const result = (yield flowResult(this.editorStore.graphState.buildGraph(entities))); if (result.error) { throw result.error; } // build explorer tree this.editorStore.explorerTreeState.buildImmutableModelTrees(); this.editorStore.explorerTreeState.build(); // NOTE: since we have already started change detection engine when we entered conflict resolution mode, we just need // to restart local change detection here // ======= (RE)START CHANGE DETECTION ======= this.editorStore.changeDetectionState.stop(); yield this.editorStore.changeDetectionState.preComputeGraphElementHashes(); this.editorStore.changeDetectionState.start(); this.editorStore.applicationStore.logService.info(LogEvent.create(LEGEND_STUDIO_APP_EVENT.CHANGE_DETECTION_RESTART__SUCCESS), '[ASNYC]'); // ======= FINISHED (RE)START CHANGE DETECTION ======= } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.logService.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.SDLC_MANAGER_FAILURE), error); this.editorStore.applicationStore.notificationService.notifyError(error); } } *buildConflictResolutionLatestRevisionEntityHashesIndex() { assertTrue(this.editorStore.isInConflictResolutionMode, 'Editor must be in conflict resolution mode to call this method'); try { // fetch latest revision const latestRevision = Revision.serialization.fromJson((yield this.editorStore.sdlcServerClient.getConflictResolutionRevision(this.sdlcState.activeProject.projectId, this.sdlcState.activeWorkspace, RevisionAlias.CURRENT))); // make sure there is no good recovery from this, at this point all users work risk conflict assertTrue(latestRevision.id === this.sdlcState.activeRevision.id, `Can't run local change detection. Current workspace revision is not the latest. Please backup your work and refresh the application`); const entities = (yield this.editorStore.sdlcServerClient.getEntitiesByRevisionFromWorkspaceInConflictResolutionMode(this.sdlcState.activeProject.projectId, this.sdlcState.activeWorkspace, this.sdlcState.activeRevision.id)); this.editorStore.changeDetectionState.conflictResolutionHeadRevisionState.setEntities(entities); yield flowResult(this.editorStore.changeDetectionState.conflictResolutionHeadRevisionState.buildEntityHashesIndex(entities, LogEvent.create(LEGEND_STUDIO_APP_EVENT.CHANGE_DETECTION_BUILD_LOCAL_HASHES_INDEX__SUCCESS))); this.editorStore.tabManagerState.refreshCurrentEntityDiffViewer(); } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.logService.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.SDLC_MANAGER_FAILURE), error); this.editorStore.applicationStore.notificationService.notifyError(error); } } *buildConflictResolutionBaseRevisionEntityHashesIndex() { assertTrue(this.editorStore.isInConflictResolutionMode, 'Editor must be in conflict resolution mode to call this method'); try { const workspaceBaseEntities = (yield this.editorStore.sdlcServerClient.getEntitiesByRevisionFromWorkspaceInConflictResolutionMode(this.sdlcState.activeProject.projectId, this.sdlcState.activeWorkspace, RevisionAlias.BASE)); this.editorStore.changeDetectionState.conflictResolutionBaseRevisionState.setEntities(workspaceBaseEntities); yield flowResult(this.editorStore.changeDetectionState.conflictResolutionBaseRevisionState.buildEntityHashesIndex(workspaceBaseEntities, LogEvent.create(LEGEND_STUDIO_APP_EVENT.CHANGE_DETECTION_BUILD_WORKSPACE_HASHES_INDEX__SUCCESS))); this.editorStore.tabManagerState.refreshCurrentEntityDiffViewer(); } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.logService.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.SDLC_MANAGER_FAILURE), error); this.editorStore.applicationStore.notificationService.notifyError(error); } } *acceptConflictResolution() { // check if the workspace is in conflict resolution mode try { const isInConflictResolutionMode = (yield flowResult(this.sdlcState.checkIfCurrentWorkspaceIsInConflictResolutionMode())); if (!isInConflictResolutionMode) { this.editorStore.applicationStore.alertService.setBlockingAlert({ message: 'Workspace is no longer in conflict resolution mode', prompt: 'Please refresh the application', }); return; } } catch (error) { assertErrorThrown(error); if (error instanceof NetworkClientError && error.response.status === HttpStatus.NOT_FOUND) { this.editorStore.applicationStore.alertService.setBlockingAlert({ message: 'Current project or workspace no longer exists', prompt: 'Please refresh the application', }); } else { this.editorStore.applicationStore.notificationService.notifyWarning('Failed to check if current workspace is in conflict resolution mode'); } return; } try { this.isAcceptingConflictResolution = true; this.editorStore.applicationStore.alertService.setBlockingAlert({ message: 'Accepting conflict resolution...', prompt: 'Please do not close the application', showLoading: true, }); const entityChanges = this.editorStore.localChangesState.computeLocalEntityChanges(); yield this.editorStore.sdlcServerClient.acceptConflictResolution(this.sdlcState.activeProject.projectId, this.sdlcState.activeWorkspace, { message: `resolving update merge conflicts for workspace from ${this.editorStore.applicationStore.config.appName} [potentially affected ${entityChanges.length === 1 ? '1 entity' : `${entityChanges.length} entities`}]`, entityChanges, revisionId: this.sdlcState.activeRevision.id, }); this.editorStore.applicationStore.navigationService.navigator.reload({ ignoreBlocking: true, }); } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.logService.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.SDLC_MANAGER_FAILURE), error); this.editorStore.applicationStore.notificationService.notifyError(error); } finally { this.isAcceptingConflictResolution = false; } } *discardConflictResolutionChanges() { // check if the workspace is in conflict resolution mode try { const isInConflictResolutionMode = (yield flowResult(this.sdlcState.checkIfCurrentWorkspaceIsInConflictResolutionMode())); if (!isInConflictResolutionMode) { this.editorStore.applicationStore.alertService.setBlockingAlert({ message: 'Workspace is no longer in conflict resolution mode', prompt: 'Please refresh the application', }); return; } } catch (error) { assertErrorThrown(error); if (error instanceof NetworkClientError && error.response.status === HttpStatus.NOT_FOUND) { this.editorStore.applicationStore.alertService.setBlockingAlert({ message: 'Current project or workspace no longer exists', prompt: 'Please refresh the application', }); } else { this.editorStore.applicationStore.notificationService.notifyWarning('Failed to check if current workspace is in conflict resolution mode'); } return; } try { this.isDiscardingConflictResolutionChanges = true; this.editorStore.applicationStore.alertService.setBlockingAlert({ message: 'Discarding conflict resolution changes...', prompt: 'Please do not close the application', showLoading: true, }); yield this.editorStore.sdlcServerClient.discardConflictResolutionChanges(this.sdlcState.activeProject.projectId, this.sdlcState.activeWorkspace); this.editorStore.applicationStore.navigationService.navigator.reload({ ignoreBlocking: true, }); } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.logService.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.SDLC_MANAGER_FAILURE), error); this.editorStore.applicationStore.notificationService.notifyError(error); } finally { this.isDiscardingConflictResolutionChanges = false; } } *abortConflictResolution() { // check if the workspace is in conflict resolution mode try { const isInConflictResolutionMode = (yield flowResult(this.sdlcState.checkIfCurrentWorkspaceIsInConflictResolutionMode())); if (!isInConflictResolutionMode) { this.editorStore.applicationStore.alertService.setBlockingAlert({ message: 'Workspace is no longer in conflict resolution mode', prompt: 'Please refresh the application', }); return; } } catch (error) { assertErrorThrown(error); if (error instanceof NetworkClientError && error.response.status === HttpStatus.NOT_FOUND) { this.editorStore.applicationStore.alertService.setBlockingAlert({ message: 'Current project or workspace no longer exists', prompt: 'Please refresh the application', }); } else { this.editorStore.applicationStore.notificationService.notifyWarning('Failed to check if current workspace is in conflict resolution mode'); } return; } try { this.isAbortingConflictResolution = true; this.editorStore.applicationStore.alertService.setBlockingAlert({ message: 'Aborting conflict resolution...', prompt: 'Please do not close the application', showLoading: true, }); yield this.editorStore.sdlcServerClient.abortConflictResolution(this.sdlcState.activeProject.projectId, this.sdlcState.activeWorkspace); this.editorStore.applicationStore.navigationService.navigator.reload({ ignoreBlocking: true, }); } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.logService.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.SDLC_MANAGER_FAILURE), error); this.editorStore.applicationStore.notificationService.notifyError(error); } finally { this.isAbortingConflictResolution = false; } } openConflictResolutionChange(diff) { const fromEntityGetter = (entityPath) => entityPath ? this.editorStore.changeDetectionState.conflictResolutionBaseRevisionState.entities.find((entity) => entity.path === entityPath) : undefined; const toEntityGetter = (entityPath) => { if (!entityPath) { return undefined; } if (this.hasResolvedAllConflicts) { // if the editor has already built the graph, we will get live entity const element = this.editorStore.graphManagerState.graph.getNullableElement(entityPath); if (!element) { return undefined; } const entity = this.editorStore.graphManagerState.graphManager.elementToEntity(element, { pruneSourceInformation: true, }); return entity; } // if the editor is still in conflict resolution phase (i.e. graph is not built yet), we will get entity from change detection or conflict resolutions const existingResolution = this.resolutions.find((resolution) => resolution.entityPath === entityPath); if (existingResolution) { return existingResolution.resolvedEntity; } // if the change is not from a conflict resolution, it must be from the list of entities of the latest revision in the workspace return this.editorStore.changeDetectionState.conflictResolutionHeadRevisionState.entities.find((entity) => entity.path === entityPath); }; const fromEntity = EntityDiff.shouldOldEntityExist(diff) ? guaranteeNonNullable(fromEntityGetter(diff.getValidatedOldPath()), `Can't find entity with path '${diff.oldPath}'`) : undefined; const toEntity = EntityDiff.shouldNewEntityExist(diff) ? guaranteeNonNullable(toEntityGetter(diff.getValidatedNewPath()), `Can't find entity with path '${diff.newPath}'`) : undefined; this.editorStore.tabManagerState.openTab(new EntityDiffViewState(this.editorStore, SPECIAL_REVISION_ALIAS.WORKSPACE_BASE, SPECIAL_REVISION_ALIAS.LOCAL, diff.oldPath, diff.newPath, fromEntity, toEntity, fromEntityGetter, toEntityGetter)); } /** * Check for remaining conflicts, if none left, prompt the users for the next action */ *promptBuildGraphAfterAllConflictsResolved() { if (!this.conflicts.length) { this.confirmHasResolvedAllConflicts(); this.editorStore.applicationStore.alertService.setBlockingAlert({ message: 'Building graph...', prompt: 'Please do not close the application', showLoading: true, }); yield flowResult(this.buildGraphInConflictResolutionMode()); this.editorStore.applicationStore.alertService.setBlockingAlert(undefined); } } } //# sourceMappingURL=WorkspaceUpdateConflictResolutionState.js.map