@finos/legend-application-studio
Version:
Legend Studio application core
382 lines • 18.9 kB
JavaScript
/**
* Copyright (c) 2020-present, Goldman Sachs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { 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