UNPKG

@finos/legend-application-studio

Version:
382 lines 18.9 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 { flowResult, action, flow, makeObservable, observable } from 'mobx'; import { assertTrue, LogEvent, hashObject, isNonNullable, guaranteeNonNullable, assertErrorThrown, deleteEntry, ActionState, } from '@finos/legend-shared'; import { LEGEND_STUDIO_APP_EVENT } from '../../../__lib__/LegendStudioEvent.js'; import { EntityChangeConflictEditorState } from '../editor-state/entity-diff-editor-state/EntityChangeConflictEditorState.js'; import { SPECIAL_REVISION_ALIAS, } from '../editor-state/entity-diff-editor-state/EntityDiffEditorState.js'; import { EntityDiffViewState } from '../editor-state/entity-diff-editor-state/EntityDiffViewState.js'; import { EntityDiff, EntityChangeType, Revision, convertEntityDiffsToEntityChanges, } from '@finos/legend-server-sdlc'; import { ActionAlertActionType, ActionAlertType, } from '@finos/legend-application'; import { AbstractConflictResolutionState } from '../AbstractConflictResolutionState.js'; class WorkspaceSyncConflictResolutionState extends AbstractConflictResolutionState { showModal = false; conflicts = []; openMergedEditorStates = []; currentDiffEditorState; resolutions = []; baseToLocalChanges = []; constructor(editorStore, sdlcState) { super(editorStore, sdlcState); makeObservable(this, { editorStore: false, sdlcState: false, conflicts: observable, resolutions: observable, mergeEditorStates: observable, currentDiffEditorState: observable, openMergedEditorStates: observable, openConflict: action, openConflictState: action, closeConflict: action, resolveConflict: action, openState: action, markConflictAsResolved: flow, showModal: observable, setShowModal: action, openEntityChangeConflict: action, openConflictResolutionChange: action, }); } get baseEntities() { return this.editorStore.changeDetectionState .workspaceLocalLatestRevisionState.entities; } get currentEntities() { return this.editorStore.graphManagerState.graph.allOwnElements.map((element) => this.editorStore.graphManagerState.graphManager.elementToEntity(element)); } get incomingEntities() { return this.editorStore.changeDetectionState .workspaceRemoteLatestRevisionState.entities; } get resolvedChanges() { return this.resolutions .map((resolution) => { const path = resolution.entityPath; const fromEntity = this.baseEntities.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 pendingConflicts() { return this.conflicts.filter((conflict) => !this.resolutions .map((resolution) => resolution.entityPath) .includes(conflict.entityPath)); } get changes() { return this.baseToLocalChanges .filter((change) => !this.pendingConflicts .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); } openState(entityDiffEditorState) { if (entityDiffEditorState instanceof EntityChangeConflictEditorState) { this.openConflictState(entityDiffEditorState); } if (entityDiffEditorState instanceof EntityDiffViewState) { this.openDiff(entityDiffEditorState); } } openConflict(conflict) { const existingMergeEditorState = this.mergeEditorStates.find((state) => state.entityPath === conflict.entityPath); if (existingMergeEditorState) { this.openEntityChangeConflict(existingMergeEditorState); return; } const baseEntityGetter = (entityPath) => entityPath ? this.baseEntities.find((e) => e.path === entityPath) : undefined; const currentChangeEntityGetter = (entityPath) => entityPath ? this.currentEntities.find((e) => e.path === entityPath) : undefined; const incomingChangeEntityGetter = (entityPath) => entityPath ? this.incomingEntities.find((e) => e.path === entityPath) : undefined; const mergeEditorState = new EntityChangeConflictEditorState(this.editorStore, this, conflict.entityPath, SPECIAL_REVISION_ALIAS.WORKSPACE_BASE, SPECIAL_REVISION_ALIAS.LOCAL, SPECIAL_REVISION_ALIAS.WORKSPACE_HEAD, baseEntityGetter(conflict.entityPath), currentChangeEntityGetter(conflict.entityPath), incomingChangeEntityGetter(conflict.entityPath), baseEntityGetter, currentChangeEntityGetter, incomingChangeEntityGetter); this.mergeEditorStates.push(mergeEditorState); this.openEntityChangeConflict(mergeEditorState); } closeConflict(conflictState) { const conflictIndex = this.openMergedEditorStates.findIndex((e) => e === conflictState); assertTrue(conflictIndex !== -1, `Can't close a tab which is not opened`); this.openMergedEditorStates.splice(conflictIndex, 1); if (this.currentDiffEditorState === conflictState) { if (this.openMergedEditorStates.length) { const openIndex = conflictIndex - 1; this.setCurrentMergeEditorState(openIndex >= 0 ? this.openMergedEditorStates[openIndex] : this.openMergedEditorStates[0]); } else { this.setCurrentMergeEditorState(undefined); } } } openConflictState(conflictState) { const existingEditorState = this.openMergedEditorStates.find((editorState) => editorState instanceof EntityChangeConflictEditorState && editorState.entityPath === conflictState.entityPath); const conflictEditorState = existingEditorState ?? conflictState; if (!existingEditorState) { this.openMergedEditorStates.push(conflictEditorState); } this.setCurrentMergeEditorState(conflictEditorState); } resolveConflict(resolution) { this.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); } get toEntityGetter() { return (entityPath) => { if (!entityPath) { return undefined; } // 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.incomingEntities.find((entity) => entity.path === entityPath); }; } openConflictResolutionChange(diff) { const fromEntityGetter = (entityPath) => entityPath ? this.baseEntities.find((entity) => entity.path === entityPath) : undefined; const fromEntity = EntityDiff.shouldOldEntityExist(diff) ? guaranteeNonNullable(fromEntityGetter(diff.getValidatedOldPath()), `Can't find entity with path '${diff.oldPath}'`) : undefined; const toEntity = EntityDiff.shouldNewEntityExist(diff) ? guaranteeNonNullable(this.toEntityGetter(diff.getValidatedNewPath()), `Can't find entity with path '${diff.newPath}'`) : undefined; this.openDiff(new EntityDiffViewState(this.editorStore, SPECIAL_REVISION_ALIAS.WORKSPACE_BASE, SPECIAL_REVISION_ALIAS.LOCAL, diff.oldPath, diff.newPath, fromEntity, toEntity, fromEntityGetter, this.toEntityGetter)); } initialize(conflicts, changes) { this.conflicts = conflicts; this.baseToLocalChanges = changes; this.setShowModal(true); } teardown() { this.setShowModal(false); this.openMergedEditorStates = []; this.mergeEditorStates = []; this.setCurrentMergeEditorState(undefined); this.conflicts = []; this.baseToLocalChanges = []; } setShowModal(val) { this.showModal = val; } setCurrentMergeEditorState(val) { this.currentDiffEditorState = val; } openEntityChangeConflict(entityChangeConflictEditorState) { const existingEditorState = this.openMergedEditorStates.find((editorState) => editorState instanceof EntityChangeConflictEditorState && editorState.entityPath === entityChangeConflictEditorState.entityPath); const conflictEditorState = existingEditorState ?? entityChangeConflictEditorState; if (!existingEditorState) { this.openMergedEditorStates.push(conflictEditorState); } this.setCurrentMergeEditorState(conflictEditorState); } openDiff(entityDiffEditorState) { const existingEditorState = this.openMergedEditorStates.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.openMergedEditorStates.push(diffEditorState); } this.setCurrentMergeEditorState(diffEditorState); } } export class WorkspaceSyncState { editorStore; sdlcState; pullChangesState = ActionState.create(); incomingRevisions = []; workspaceSyncConflictResolutionState; constructor(editorStore, sdlcState) { makeObservable(this, { pullChangesState: observable, incomingRevisions: observable, workspaceSyncConflictResolutionState: observable, resetConflictState: action, setIncomingRevisions: action, fetchIncomingRevisions: flow, pullChanges: flow, loadChanges: flow, forcePull: flow, applyResolutionChanges: flow, }); this.editorStore = editorStore; this.sdlcState = sdlcState; this.workspaceSyncConflictResolutionState = new WorkspaceSyncConflictResolutionState(editorStore, sdlcState); } resetConflictState() { this.workspaceSyncConflictResolutionState.teardown(); this.workspaceSyncConflictResolutionState = new WorkspaceSyncConflictResolutionState(this.editorStore, this.sdlcState); } setIncomingRevisions(revisions) { this.incomingRevisions = revisions; } *fetchIncomingRevisions() { try { assertTrue(this.sdlcState.isWorkspaceOutOfSync); const revisions = (yield this.editorStore.sdlcServerClient.getRevisions(this.sdlcState.activeProject.projectId, this.sdlcState.activeWorkspace, this.sdlcState.activeRevision.committedAt, this.sdlcState.activeRemoteWorkspaceRevision.committedAt)).map((v) => Revision.serialization.fromJson(v)); this.setIncomingRevisions(revisions.filter((r) => r.id !== this.sdlcState.activeRevision.id)); } catch (error) { this.setIncomingRevisions([]); assertErrorThrown(error); } } *pullChanges() { try { assertTrue(this.sdlcState.isWorkspaceOutOfSync); this.editorStore.applicationStore.alertService.setBlockingAlert({ message: `Pulling latest changes...`, showLoading: true, }); this.pullChangesState.inProgress(); const changes = this.editorStore.changeDetectionState.workspaceLocalLatestRevisionState .changes; let conflicts = []; if (changes.length) { yield flowResult(this.editorStore.changeDetectionState.computeAggregatedWorkspaceRemoteChanges()); conflicts = this.editorStore.changeDetectionState.potentialWorkspacePullConflicts; } if (conflicts.length) { this.editorStore.applicationStore.alertService.setBlockingAlert(undefined); this.editorStore.applicationStore.alertService.setActionAlertInfo({ message: 'Conflicts found while pulling changes', prompt: 'You can either force-pull (override local changes) or resolve these conflicts manually', type: ActionAlertType.CAUTION, actions: [ { label: 'Resolve merge conflicts', default: true, handler: () => this.workspaceSyncConflictResolutionState.initialize(conflicts, this.editorStore.changeDetectionState .aggregatedWorkspaceRemoteChanges), type: ActionAlertActionType.STANDARD, }, { label: 'Force pull', type: ActionAlertActionType.PROCEED_WITH_CAUTION, handler: () => { flowResult(this.forcePull()).catch(this.editorStore.applicationStore.alertUnhandledError); }, }, ], }); return; } const localChanges = this.editorStore.localChangesState.computeLocalEntityChanges(); yield flowResult(this.loadChanges(localChanges)); } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.notificationService.notifyError(`Can't pull changes. Error: ${error.message}`); } finally { this.pullChangesState.complete(); } } *loadChanges(changes) { this.editorStore.sdlcState.setCurrentRevision(this.sdlcState.activeRemoteWorkspaceRevision); const entities = this.editorStore.changeDetectionState.workspaceRemoteLatestRevisionState .entities; this.editorStore.changeDetectionState.workspaceLocalLatestRevisionState.setEntities(entities); this.resetConflictState(); yield flowResult(this.editorStore.changeDetectionState.workspaceLocalLatestRevisionState.buildEntityHashesIndex(entities, LogEvent.create(LEGEND_STUDIO_APP_EVENT.CHANGE_DETECTION_BUILD_LOCAL_HASHES_INDEX__SUCCESS))); this.setIncomingRevisions([]); this.editorStore.changeDetectionState.setAggregatedWorkspaceRemoteChanges([]); this.editorStore.changeDetectionState.setPotentialWorkspacePullConflicts([]); yield flowResult(this.editorStore.graphState.loadEntityChangesToGraph(changes, // we create new entities to not override the initial entities on `workspaceLatestRevisionState` used for change detection entities.map((e) => ({ classifierPath: e.classifierPath, path: e.path, content: e.content, })))); } *forcePull() { try { const changes = this.editorStore.localChangesState.computeLocalEntityChanges(); yield flowResult(this.loadChanges(changes)); this.editorStore.applicationStore.notificationService.notifySuccess('Workspace changes were force-pulled'); } catch (error) { assertErrorThrown(error); this.resetConflictState(); this.editorStore.applicationStore.notificationService.notifyError(`Can't force-pull remote workspace changes. Error: ${error.message}`); } finally { this.editorStore.applicationStore.alertService.setBlockingAlert(undefined); } } *applyResolutionChanges() { try { this.editorStore.applicationStore.alertService.setBlockingAlert({ message: `Applying resolutions and reloading graph...`, showLoading: true, }); const changes = convertEntityDiffsToEntityChanges(this.workspaceSyncConflictResolutionState.changes, this.workspaceSyncConflictResolutionState.toEntityGetter); yield flowResult(this.loadChanges(changes)); } catch (error) { assertErrorThrown(error); this.resetConflictState(); this.editorStore.applicationStore.notificationService.notifyError(`Can't apply resolutions to local workspace. Error: ${error.message}`); } finally { this.editorStore.applicationStore.alertService.setBlockingAlert(undefined); } } } //# sourceMappingURL=WorkspaceSyncState.js.map