Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

load and apply patch changes #748

Merged
merged 2 commits into from Dec 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
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">
MauricioUyaguari marked this conversation as resolved.
Show resolved Hide resolved
<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