Skip to content

Commit

Permalink
support load and apply patches (#748)
Browse files Browse the repository at this point in the history
  • Loading branch information
MauricioUyaguari committed Dec 30, 2021
1 parent 81f87eb commit f493059
Show file tree
Hide file tree
Showing 11 changed files with 519 additions and 6 deletions.
4 changes: 4 additions & 0 deletions .changeset/coffee-test-maker.md
@@ -0,0 +1,4 @@
---
'@finos/legend-server-sdlc': patch
'@finos/legend-shared': patch
---
5 changes: 5 additions & 0 deletions .changeset/swift-gifts-shake.md
@@ -0,0 +1,5 @@
---
'@finos/legend-studio': minor
---

Support upload and load patch.
13 changes: 13 additions & 0 deletions packages/legend-server-sdlc/src/models/entity/EntityChange.ts
Expand Up @@ -14,6 +14,9 @@
* limitations under the License.
*/

import { createModelSchema, raw, optional, primitive } from 'serializr';
import { SerializationFactory } from '@finos/legend-shared';

export enum EntityChangeType {
CREATE = 'CREATE',
DELETE = 'DELETE',
Expand All @@ -29,4 +32,14 @@ export class EntityChange {
classifierPath?: string;
newEntityPath?: string;
content?: Record<PropertyKey, unknown>;

static readonly serialization = new SerializationFactory(
createModelSchema(EntityChange, {
type: primitive(),
entityPath: primitive(),
classifierPath: optional(primitive()),
newEntityPath: optional(primitive()),
content: optional(raw()),
}),
);
}
33 changes: 33 additions & 0 deletions packages/legend-shared/src/application/BrowserUtils.ts
@@ -0,0 +1,33 @@
/**
* 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 { isString } from '../error/AssertionUtils';
import { UnsupportedOperationError } from '../error/ErrorUtils';

export const readFileAsText = (file: File): Promise<string> =>
new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = (): void => {
const result = fileReader.result;
if (isString(result)) {
resolve(result);
} else {
throw new UnsupportedOperationError(`Cant read file`);
}
};
fileReader.onerror = reject;
fileReader.readAsText(file);
});
1 change: 1 addition & 0 deletions packages/legend-shared/src/index.ts
Expand Up @@ -36,6 +36,7 @@ export * from './application/SerializationUtils';
export * from './application/RandomizerUtils';
export * from './application/ActionState';
export * from './application/AbstractPluginManager';
export * from './application/BrowserUtils';

export * from './data-structure/Pair';
export * from './data-structure/Stack';
Expand Down
110 changes: 107 additions & 3 deletions packages/legend-studio/src/components/editor/side-bar/LocalChanges.tsx
Expand Up @@ -15,18 +15,103 @@
*/

import { observer } from 'mobx-react-lite';
import { clsx, PanelLoadingIndicator } from '@finos/legend-art';
import { clsx, PanelLoadingIndicator, TimesIcon } from '@finos/legend-art';
import { EntityDiffViewState } from '../../../stores/editor-state/entity-diff-editor-state/EntityDiffViewState';
import { EntityDiffSideBarItem } from '../../editor/edit-panel/diff-editor/EntityDiffView';
import { FaInfoCircle, FaDownload } from 'react-icons/fa';
import { FaInfoCircle, FaDownload, FaUpload } from 'react-icons/fa';
import { MdRefresh } from 'react-icons/md';
import { GoSync } from 'react-icons/go';
import { LEGEND_STUDIO_TEST_ID } from '../../LegendStudioTestID';
import { flowResult } from 'mobx';
import type { EntityDiff } from '@finos/legend-server-sdlc';
import type { EntityChange, EntityDiff } from '@finos/legend-server-sdlc';
import { entityDiffSorter } from '../../../stores/EditorSDLCState';
import { useEditorStore } from '../EditorStoreProvider';
import { useApplicationStore } from '@finos/legend-application';
import { Dialog } from '@material-ui/core';

const PatchLoader = observer(() => {
const editorStore = useEditorStore();
const localChangesState = editorStore.localChangesState;
const patchState = localChangesState.patchLoaderState;
const onClose = (): void => patchState.closeModal();
const onChange: React.ChangeEventHandler<HTMLInputElement> = (event) => {
const file = event.target.files?.[0];
if (file) {
patchState.loadPatchFile(file);
}
};
const upload = (): void => {
patchState.applyChanges();
};
const deleteChange = (change: EntityChange): void =>
patchState.deleteChange(change);
return (
<Dialog onClose={onClose} open={patchState.showModal}>
<div className="modal modal--dark modal--scrollable patch-loader">
<div className="modal__header">
<div className="modal__title">
<div className="modal__title__label">Patch Loader</div>
</div>
</div>
<div className="modal__body">
<PanelLoadingIndicator isLoading={patchState.isLoadingChanges} />
<div>
<input
id="upload-file"
type="file"
name="myFiles"
onChange={onChange}
/>
</div>
{Boolean(patchState.overiddingChanges.length) && (
<div className="panel__content__form__section">
<div className="panel__content__form__section__header__label">
Overriding Changes
</div>
<div className="panel__content__form__section__header__prompt">
The following element changes will be overridden by the patch
</div>
<div className="panel__content__form__section__list">
<div className="panel__content__form__section__list__items">
{patchState.overiddingChanges.map((value) => (
<div
key={value.entityPath}
className="panel__content__form__section__list__item"
>
<div className="panel__content__form__section__list__item__value">
{value.entityPath}
</div>
<div className="panel__content__form__section__list__item__actions">
<button
title="Remove change"
className="panel__content__form__section__list__item__remove-btn"
onClick={(): void => deleteChange(value)}
tabIndex={-1}
>
<TimesIcon />
</button>
</div>
</div>
))}
</div>
</div>
</div>
)}
</div>
<div className="modal__footer">
<button
type="button"
className="btn btn--dark blocking-alert__action--standard"
onClick={upload}
disabled={!patchState.changes?.length || !patchState.isValidPatch}
>
Apply Patch
</button>
</div>
</div>
</Dialog>
);
});

export const LocalChanges = observer(() => {
const editorStore = useEditorStore();
Expand All @@ -35,6 +120,10 @@ export const LocalChanges = observer(() => {
// Actions
const downloadLocalChanges = (): void =>
localChangesState.downloadLocalChanges();
const uploadPatchChanges = (): void =>
localChangesState.patchLoaderState.openModal(
editorStore.graphState.computeLocalEntityChanges(),
);
const syncingWithWorkspace = applicationStore.guaranteeSafeAction(() =>
flowResult(localChangesState.syncWithWorkspace()),
);
Expand Down Expand Up @@ -79,6 +168,20 @@ export const LocalChanges = observer(() => {
>
<FaDownload />
</button>
<button
className="panel__header__action side-bar__header__action local-changes__download-patch-btn"
onClick={uploadPatchChanges}
disabled={
isDispatchingAction ||
editorStore.workspaceUpdaterState.isUpdatingWorkspace ||
!editorStore.changeDetectionState.isChangeDetectionRunning ||
!editorStore.isInFormMode
}
tabIndex={-1}
title="Upload local entity changes"
>
<FaUpload />
</button>
<button
className={clsx(
'panel__header__action side-bar__header__action local-changes__refresh-btn',
Expand Down Expand Up @@ -116,6 +219,7 @@ export const LocalChanges = observer(() => {
</div>
<div className="panel__content side-bar__content">
<PanelLoadingIndicator isLoading={isDispatchingAction} />
{localChangesState.patchLoaderState.showModal && <PatchLoader />}
<div className="panel side-bar__panel">
<div className="panel__header">
<div className="panel__header__title">
Expand Down
25 changes: 25 additions & 0 deletions packages/legend-studio/src/stores/EditorGraphState.ts
Expand Up @@ -392,6 +392,31 @@ export class EditorGraphState {
return entityChanges;
}

*loadEntityChangesToGraph(changes: EntityChange[]): GeneratorFn<void> {
try {
assertTrue(
this.editorStore.isInFormMode,
'Applying changes only supported in form mode',
);
const updatedEntities =
this.editorStore.localChangesState.patchLoaderState.applyEntityChanges(
this.editorStore.graphManagerState.graph.allOwnElements.map(
(element) =>
this.editorStore.graphManagerState.graphManager.elementToEntity(
element,
),
),
changes,
);
yield flowResult(this.updateGraphAndApplication(updatedEntities));
} catch (error) {
assertErrorThrown(error);
this.editorStore.applicationStore.notifyError(
`Unable to load entity changes: ${error.message}`,
);
}
}

// FIXME: when we support showing multiple notifications, we can take this options out as the only users of this
// is delete element flow, where we want to say `re-compiling graph after deletion`, but because compilation
// sometimes is so fast, the message flashes, so we want to combine with the message in this method
Expand Down

0 comments on commit f493059

Please sign in to comment.