From 26ab5eefc53f1f505a9bc5d59749cc6d4bcd90f4 Mon Sep 17 00:00:00 2001 From: An Phi Date: Mon, 30 Dec 2024 03:25:19 -0500 Subject: [PATCH] datacube: support saving/saving as new query --- .changeset/honest-pillows-end.md | 18 ++ .../LegendDataCubeNewQueryBuilder.tsx | 31 +-- .../LegendDataCubeQueryBuilder.tsx | 89 +----- .../LegendDataCubeQuerySaver.tsx | 106 +++++++ .../src/stores/LegendDataCubeBaseStore.ts | 2 - .../LegendDataCubeNewQueryState.tsx | 59 ++-- .../LegendDataCubeQueryBuilderStore.ts | 124 --------- .../LegendDataCubeQueryBuilderStore.tsx | 260 ++++++++++++++++++ .../LegendDataCubeSourceBuilderState.ts | 16 +- .../LegendQueryDataCubeSourceBuilderState.ts | 10 +- .../LegendREPLPublishDataCubeAlert.tsx | 2 +- packages/legend-art/src/icon/DataCubeIcon.tsx | 9 +- .../src/components/core/DataCubeAlert.tsx | 12 +- .../src/components/core/DataCubeFormUtils.tsx | 1 - .../components/core/DataCubeLayoutManager.tsx | 2 +- .../components/view/editor/DataCubeEditor.tsx | 4 +- .../view/extend/DataCubeColumnEditor.tsx | 6 +- .../view/filter/DataCubeFilterEditor.tsx | 4 +- packages/legend-data-cube/src/index.tsx | 3 +- .../src/stores/DataCubeOptions.ts | 2 + .../src/stores/DataCubeState.tsx | 4 + .../src/stores/core/DataCubeQueryBuilder.ts | 1 + ..._Data_Quality_PureGraphManagerExtension.ts | 1 - .../action/query/PersistentDataCubeQuery.ts | 6 + 24 files changed, 485 insertions(+), 287 deletions(-) create mode 100644 .changeset/honest-pillows-end.md create mode 100644 packages/legend-application-data-cube/src/components/query-builder/LegendDataCubeQuerySaver.tsx delete mode 100644 packages/legend-application-data-cube/src/stores/query-builder/LegendDataCubeQueryBuilderStore.ts create mode 100644 packages/legend-application-data-cube/src/stores/query-builder/LegendDataCubeQueryBuilderStore.tsx diff --git a/.changeset/honest-pillows-end.md b/.changeset/honest-pillows-end.md new file mode 100644 index 00000000000..55e6957d158 --- /dev/null +++ b/.changeset/honest-pillows-end.md @@ -0,0 +1,18 @@ +--- +'@finos/legend-application-data-cube-deployment': patch +'@finos/legend-application-data-cube-bootstrap': patch +'@finos/legend-vscode-extension-dependencies': patch +'@finos/legend-extension-dsl-data-quality': patch +'@finos/legend-extension-dsl-data-space': patch +'@finos/legend-application-data-cube': patch +'@finos/legend-application-pure-ide': patch +'@finos/legend-application-studio': patch +'@finos/legend-application-query': patch +'@finos/legend-application-repl': patch +'@finos/legend-query-builder': patch +'@finos/legend-application': patch +'@finos/legend-data-cube': patch +'@finos/legend-shared': patch +'@finos/legend-graph': patch +'@finos/legend-art': patch +--- diff --git a/packages/legend-application-data-cube/src/components/query-builder/LegendDataCubeNewQueryBuilder.tsx b/packages/legend-application-data-cube/src/components/query-builder/LegendDataCubeNewQueryBuilder.tsx index 792654e1e6a..454766a2687 100644 --- a/packages/legend-application-data-cube/src/components/query-builder/LegendDataCubeNewQueryBuilder.tsx +++ b/packages/legend-application-data-cube/src/components/query-builder/LegendDataCubeNewQueryBuilder.tsx @@ -43,7 +43,7 @@ export const LegendDataCubeNewQueryBuilder = observer( return ( <> -
+
@@ -94,37 +94,22 @@ export const LegendDataCubeNewQueryBuilder = observer(
- state.display.close()} autoFocus={true}> - Cancel - + state.display.close()}>Cancel { - // editor.applyChanges(); - state.generateQuery().then((query) => {}); - state.display.close(); + state + .finalize() + .catch((error) => state.engine.alertUnhandledError(error)); }} > OK
- //
- // ); }, ); diff --git a/packages/legend-application-data-cube/src/components/query-builder/LegendDataCubeQueryBuilder.tsx b/packages/legend-application-data-cube/src/components/query-builder/LegendDataCubeQueryBuilder.tsx index d6ae3bf140f..71385bc883b 100644 --- a/packages/legend-application-data-cube/src/components/query-builder/LegendDataCubeQueryBuilder.tsx +++ b/packages/legend-application-data-cube/src/components/query-builder/LegendDataCubeQueryBuilder.tsx @@ -23,7 +23,6 @@ import { type DataCubeState, type DataCubeSettingValues, } from '@finos/legend-data-cube'; -import { formatDate } from '@finos/legend-shared'; import { DataCubeIcon, DropdownMenu, @@ -42,77 +41,9 @@ import { import { useEffect } from 'react'; import { LegendDataCubeSettingStorageKey } from '../../__lib__/LegendDataCubeSetting.js'; -// const CreateQueryDialog = observer( -// (props: { view: LegendCubeViewer; store: LegendDataCubeBaseStore }) => { -// const { store } = props; -// const close = (): void => store.setSaveModal(false); -// const [queryName, setQueryName] = useState(''); -// const create = (): void => { -// flowResult(store.saveQuery(queryName)).catch( -// store.application.alertUnhandledError, -// ); -// }; -// const isEmptyName = !queryName; -// // name -// const nameInputRef = useRef(null); -// const setFocus = (): void => { -// nameInputRef.current?.focus(); -// }; -// const changeName: React.ChangeEventHandler = (event) => { -// setQueryName(event.target.value); -// }; -// useEffect(() => { -// setTimeout(() => setFocus(), 1); -// }, []); -// return ( -// -// -// -// -// -// -//
-// -//
-//
-//
-// -// -// -//
-//
-// ); -// }, -// ); - const LegendDataCubeQueryBuilderHeader = observer( (props: { dataCube?: DataCubeState | undefined }) => { const store = useLegendDataCubeQueryBuilderStore(); - const { dataCube } = props; return (
@@ -122,7 +53,6 @@ const LegendDataCubeQueryBuilderHeader = observer( disabled={true} > Load Query - New Query - + store.saverDisplay.open()} + > Save Query
@@ -227,6 +162,7 @@ const LegendDataCubeBlankQueryBuilder = observer(() => { export const LegendDataCubeQueryBuilder = withLegendDataCubeQueryBuilderStore( observer(() => { const store = useLegendDataCubeQueryBuilderStore(); + const builder = store.builder; const application = store.application; const params = useParams(); const queryId = params[LEGEND_DATA_CUBE_ROUTE_PATTERN_TOKEN.QUERY_ID]; @@ -246,19 +182,16 @@ export const LegendDataCubeQueryBuilder = withLegendDataCubeQueryBuilderStore( } }, [store, queryId]); - if (!store.builder) { + if (!builder) { return ; } return ( ( diff --git a/packages/legend-application-data-cube/src/components/query-builder/LegendDataCubeQuerySaver.tsx b/packages/legend-application-data-cube/src/components/query-builder/LegendDataCubeQuerySaver.tsx new file mode 100644 index 00000000000..d2853c993b4 --- /dev/null +++ b/packages/legend-application-data-cube/src/components/query-builder/LegendDataCubeQuerySaver.tsx @@ -0,0 +1,106 @@ +/** + * 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 { + DEFAULT_REPORT_NAME, + FormButton, + FormTextInput, +} from '@finos/legend-data-cube'; +import { observer } from 'mobx-react-lite'; +import { useEffect, useState } from 'react'; +import { useLegendDataCubeQueryBuilderStore } from './LegendDataCubeQueryBuilderStoreProvider.js'; +import { guaranteeNonNullable } from '@finos/legend-shared'; + +export const LegendDataCubeQuerySaver = observer(() => { + const [name, setName] = useState(DEFAULT_REPORT_NAME); + const store = useLegendDataCubeQueryBuilderStore(); + const builder = guaranteeNonNullable(store.builder); + + useEffect(() => { + setName(builder.persistentQuery?.name ?? DEFAULT_REPORT_NAME); + }, [builder]); + + return ( + <> +
+
+
+
+
+ Name: +
+ { + setName(event.target.value.trim()); + }} + autoFocus={true} + /> +
+
+
+
+
+ store.saverDisplay.close()}> + Cancel + + {builder.persistentQuery ? ( + // updating existing query + <> + { + store + .saveQuery(name, false) + .catch((error) => store.engine.alertUnhandledError(error)); + }} + > + Save + + { + store + .saveQuery(name, true) + .catch((error) => store.engine.alertUnhandledError(error)); + }} + > + Save As + + + ) : ( + // creating new query + <> + { + store + .createQuery(name) + .catch((error) => store.engine.alertUnhandledError(error)); + }} + > + Save + + + )} +
+ + ); +}); diff --git a/packages/legend-application-data-cube/src/stores/LegendDataCubeBaseStore.ts b/packages/legend-application-data-cube/src/stores/LegendDataCubeBaseStore.ts index 336a60548b4..445b952000e 100644 --- a/packages/legend-application-data-cube/src/stores/LegendDataCubeBaseStore.ts +++ b/packages/legend-application-data-cube/src/stores/LegendDataCubeBaseStore.ts @@ -62,8 +62,6 @@ export class LegendDataCubeBaseStore { readonly graphManager: V1_PureGraphManager; readonly engineServerClient: V1_EngineServerClient; readonly engine: LegendDataCubeDataCubeEngine; - - readonly startTime = Date.now(); readonly initState = ActionState.create(); readonly dataCubeSettings: DataCubeSetting[]; diff --git a/packages/legend-application-data-cube/src/stores/query-builder/LegendDataCubeNewQueryState.tsx b/packages/legend-application-data-cube/src/stores/query-builder/LegendDataCubeNewQueryState.tsx index a4b980ea287..2ef2048b5e7 100644 --- a/packages/legend-application-data-cube/src/stores/query-builder/LegendDataCubeNewQueryState.tsx +++ b/packages/legend-application-data-cube/src/stores/query-builder/LegendDataCubeNewQueryState.tsx @@ -16,15 +16,13 @@ import { action, makeObservable, observable } from 'mobx'; import { + ActionState, + assertErrorThrown, IllegalStateError, UnsupportedOperationError, } from '@finos/legend-shared'; import { LegendQueryDataCubeSourceBuilderState } from './source-builder/LegendQueryDataCubeSourceBuilderState.js'; -import type { - LegendDataCubeApplicationStore, - LegendDataCubeBaseStore, -} from '../LegendDataCubeBaseStore.js'; -import type { V1_PureGraphManager } from '@finos/legend-graph'; +import type { LegendDataCubeApplicationStore } from '../LegendDataCubeBaseStore.js'; import { LegendDataCubeSourceBuilderType, type LegendDataCubeSourceBuilderState, @@ -38,26 +36,29 @@ import { import type { LegendDataCubeDataCubeEngine } from '../LegendDataCubeDataCubeEngine.js'; import { LegendDataCubeNewQueryBuilder } from '../../components/query-builder/LegendDataCubeNewQueryBuilder.js'; import { AdhocQueryDataCubeSourceBuilderState } from './source-builder/AdhocQueryDataCubeSourceBuilderState.js'; +import { + LegendDataCubeQueryBuilderState, + type LegendDataCubeQueryBuilderStore, +} from './LegendDataCubeQueryBuilderStore.js'; export class LegendDataCubeNewQueryState { readonly application: LegendDataCubeApplicationStore; - readonly baseStore: LegendDataCubeBaseStore; - readonly graphManager: V1_PureGraphManager; + readonly store: LegendDataCubeQueryBuilderStore; readonly engine: LegendDataCubeDataCubeEngine; readonly display: DisplayState; + readonly finalizeState = ActionState.create(); sourceBuilder: LegendDataCubeSourceBuilderState; - constructor(baseStore: LegendDataCubeBaseStore) { + constructor(store: LegendDataCubeQueryBuilderStore) { makeObservable(this, { sourceBuilder: observable, changeSourceBuilder: action, }); - this.application = baseStore.application; - this.baseStore = baseStore; - this.graphManager = baseStore.graphManager; - this.engine = baseStore.engine; + this.application = store.application; + this.store = store; + this.engine = store.engine; this.display = this.engine.layout.newDisplay( 'New Query', () => , @@ -84,9 +85,9 @@ export class LegendDataCubeNewQueryState { ): LegendDataCubeSourceBuilderState { switch (type) { case LegendDataCubeSourceBuilderType.LEGEND_QUERY: - return new LegendQueryDataCubeSourceBuilderState(this.baseStore); + return new LegendQueryDataCubeSourceBuilderState(this); case LegendDataCubeSourceBuilderType.ADHOC_QUERY: - return new AdhocQueryDataCubeSourceBuilderState(this.baseStore); + return new AdhocQueryDataCubeSourceBuilderState(this); default: throw new UnsupportedOperationError( `Can't create source builder for unsupported type '${type}'`, @@ -94,17 +95,29 @@ export class LegendDataCubeNewQueryState { } } - async generateQuery(): Promise { + async finalize() { if (!this.sourceBuilder.isValid) { throw new IllegalStateError(`Can't generate query: source is not valid`); } - const source = await this.sourceBuilder.build(); - const query = new DataCubeQuery(); - const processedSource = await this.engine.processQuerySource(source); - query.source = source; - query.query = await this.engine.getValueSpecificationCode( - _selectFunction(processedSource.columns), - ); - return query; + + this.finalizeState.inProgress(); + try { + const source = await this.sourceBuilder.build(); + const query = new DataCubeQuery(); + const processedSource = await this.engine.processQuerySource(source); + query.source = source; + query.query = await this.engine.getValueSpecificationCode( + _selectFunction(processedSource.columns), + ); + this.store.setBuilder(new LegendDataCubeQueryBuilderState(query)); + this.display.close(); + this.finalizeState.pass(); + } catch (error) { + assertErrorThrown(error); + this.engine.alertError(error, { + message: `Query Creation Failure: ${error.message}`, + }); + this.finalizeState.fail(); + } } } diff --git a/packages/legend-application-data-cube/src/stores/query-builder/LegendDataCubeQueryBuilderStore.ts b/packages/legend-application-data-cube/src/stores/query-builder/LegendDataCubeQueryBuilderStore.ts deleted file mode 100644 index 9fbea2c9aac..00000000000 --- a/packages/legend-application-data-cube/src/stores/query-builder/LegendDataCubeQueryBuilderStore.ts +++ /dev/null @@ -1,124 +0,0 @@ -/** - * 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, makeObservable, observable } from 'mobx'; -import type { - LegendDataCubeApplicationStore, - LegendDataCubeBaseStore, -} from '../LegendDataCubeBaseStore.js'; -import { DataCubeQuery } from '@finos/legend-data-cube'; -import { LegendDataCubeNewQueryState } from './LegendDataCubeNewQueryState.js'; -import type { PersistentDataCubeQuery } from '@finos/legend-graph'; -import { ActionState, assertErrorThrown, uuid } from '@finos/legend-shared'; -import type { LegendDataCubeDataCubeEngine } from '../LegendDataCubeDataCubeEngine.js'; - -class LegendDataCubeQueryBuilderState { - uuid = uuid(); - persistentQuery?: PersistentDataCubeQuery | undefined; - query!: DataCubeQuery; - - constructor( - query: DataCubeQuery, - persistentQuery?: PersistentDataCubeQuery | undefined, - ) { - this.query = query; - this.persistentQuery = persistentQuery; - } -} - -export class LegendDataCubeQueryBuilderStore { - readonly application: LegendDataCubeApplicationStore; - readonly baseStore: LegendDataCubeBaseStore; - readonly engine: LegendDataCubeDataCubeEngine; - - readonly newQueryState: LegendDataCubeNewQueryState; - readonly loadQueryState = ActionState.create(); - builder?: LegendDataCubeQueryBuilderState | undefined; - - constructor(baseStore: LegendDataCubeBaseStore) { - makeObservable(this, { - builder: observable, - setBuilder: action, - }); - - this.application = baseStore.application; - this.baseStore = baseStore; - this.engine = baseStore.engine; - this.newQueryState = new LegendDataCubeNewQueryState(baseStore); - } - - setBuilder(val: LegendDataCubeQueryBuilderState | undefined) { - this.builder = val; - } - - async loadQuery(queryId: string | undefined) { - if (queryId !== this.builder?.persistentQuery?.id) { - if (!queryId) { - this.setBuilder(undefined); - return; - } - - this.loadQueryState.inProgress(); - - try { - const persistentQuery = - await this.baseStore.graphManager.getDataCubeQuery(queryId); - const query = DataCubeQuery.serialization.fromJson( - persistentQuery.content, - ); - this.setBuilder( - new LegendDataCubeQueryBuilderState(query, persistentQuery), - ); - this.loadQueryState.pass(); - } catch (error) { - assertErrorThrown(error); - this.engine.alertError(error, { - message: `Query Load Failure: ${error.message}`, - }); - this.loadQueryState.fail(); - } - } - } -} - -// *saveQuery(name: string): GeneratorFn { -// try { -// this.saveModalState.inProgress(); -// const view = guaranteeNonNullable(this.cubeViewer); -// const content = serializeDataCubeQueryConent( -// createQueryBuilderContent(view.source), -// ); -// const cubeQuery = new PersistentDataCubeQuery(); -// cubeQuery.content = content; -// cubeQuery.name = name; -// cubeQuery.id = uuid(); -// const querySaved = -// (yield this.context.graphManagerState.graphManager.createQueryDataCube( -// cubeQuery, -// )) as unknown as PersistentDataCubeQuery; -// this.savedQuery = querySaved; -// // TODO: fix reload -// this.application.navigationService.navigator.goToLocation( -// generatedSavedQueryUrl(querySaved.id), -// ); -// this.setSaveModal(false); -// this.saveModalState.complete(); -// } catch (error) { -// assertErrorThrown(error); -// this.saveModalState.fail(); -// this.application.notificationService.notifyError(`Unable to save query`); -// } -// } diff --git a/packages/legend-application-data-cube/src/stores/query-builder/LegendDataCubeQueryBuilderStore.tsx b/packages/legend-application-data-cube/src/stores/query-builder/LegendDataCubeQueryBuilderStore.tsx new file mode 100644 index 00000000000..9525568f6e0 --- /dev/null +++ b/packages/legend-application-data-cube/src/stores/query-builder/LegendDataCubeQueryBuilderStore.tsx @@ -0,0 +1,260 @@ +/** + * 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, makeObservable, observable } from 'mobx'; +import type { + LegendDataCubeApplicationStore, + LegendDataCubeBaseStore, +} from '../LegendDataCubeBaseStore.js'; +import { + DataCubeConfiguration, + DataCubeQuery, + DEFAULT_SMALL_ALERT_WINDOW_CONFIG, + type DataCubeState, + type DisplayState, +} from '@finos/legend-data-cube'; +import { LegendDataCubeNewQueryState } from './LegendDataCubeNewQueryState.js'; +import { PersistentDataCubeQuery } from '@finos/legend-graph'; +import { + ActionState, + assertErrorThrown, + formatDate, + uuid, +} from '@finos/legend-shared'; +import type { LegendDataCubeDataCubeEngine } from '../LegendDataCubeDataCubeEngine.js'; +import { LegendDataCubeQuerySaver } from '../../components/query-builder/LegendDataCubeQuerySaver.js'; +import { generateQueryBuilderRoute } from '../../__lib__/LegendDataCubeNavigation.js'; + +export class LegendDataCubeQueryBuilderState { + startTime = Date.now(); + query!: DataCubeQuery; + persistentQuery?: PersistentDataCubeQuery | undefined; + dataCube?: DataCubeState | undefined; + + constructor( + query: DataCubeQuery, + persistentQuery?: PersistentDataCubeQuery | undefined, + ) { + makeObservable(this, { + dataCube: observable, + setDataCube: action, + + query: observable, + persistentQuery: observable, + setPersistentQuery: action, + }); + + this.query = query; + this.persistentQuery = persistentQuery; + } + + setDataCube(val: DataCubeState | undefined) { + this.dataCube = val; + } + + setPersistentQuery(val: PersistentDataCubeQuery) { + this.persistentQuery = val; + this.query = DataCubeQuery.serialization.fromJson(val.content); + this.startTime = Date.now(); + } +} + +export class LegendDataCubeQueryBuilderStore { + readonly application: LegendDataCubeApplicationStore; + readonly baseStore: LegendDataCubeBaseStore; + readonly engine: LegendDataCubeDataCubeEngine; + + readonly newQueryState: LegendDataCubeNewQueryState; + + readonly saveQueryState = ActionState.create(); + readonly saverDisplay: DisplayState; + + readonly loadQueryState = ActionState.create(); + // loader: LegendDataCubeQueryLoaderState; + builder?: LegendDataCubeQueryBuilderState | undefined; + + constructor(baseStore: LegendDataCubeBaseStore) { + makeObservable(this, { + builder: observable, + setBuilder: action, + }); + + this.application = baseStore.application; + this.baseStore = baseStore; + this.engine = baseStore.engine; + this.newQueryState = new LegendDataCubeNewQueryState(this); + this.saverDisplay = this.engine.layout.newDisplay( + 'Save Query', + () => , + { + ...DEFAULT_SMALL_ALERT_WINDOW_CONFIG, + height: 140, + }, + ); + } + + setBuilder(val: LegendDataCubeQueryBuilderState | undefined) { + this.builder = val; + } + + private updateWindowTitle(persistentQuery: PersistentDataCubeQuery) { + this.application.layoutService.setWindowTitle( + `\u229E ${persistentQuery.name}${this.builder ? ` - ${formatDate(new Date(this.builder.startTime), 'HH:mm:ss EEE MMM dd yyyy')}` : ''}`, + ); + } + + async loadQuery(queryId: string | undefined) { + if (queryId !== this.builder?.persistentQuery?.id) { + if (!queryId) { + this.setBuilder(undefined); + return; + } + + this.loadQueryState.inProgress(); + + try { + const persistentQuery = + await this.baseStore.graphManager.getDataCubeQuery(queryId); + const query = DataCubeQuery.serialization.fromJson( + persistentQuery.content, + ); + this.setBuilder( + new LegendDataCubeQueryBuilderState(query, persistentQuery), + ); + this.updateWindowTitle(persistentQuery); + this.loadQueryState.pass(); + } catch (error) { + assertErrorThrown(error); + this.engine.alertError(error, { + message: `Query Load Failure: ${error.message}`, + }); + this.loadQueryState.fail(); + } + } + } + + private async generatePersistentQuery( + dataCube: DataCubeState, + name: string, + existingPersistentQuery?: PersistentDataCubeQuery | undefined, + ) { + const currentSnapshot = dataCube.view.snapshotManager.currentSnapshot; + + const query = new DataCubeQuery(); + query.source = dataCube.query.source; + query.configuration = DataCubeConfiguration.serialization.fromJson( + currentSnapshot.data.configuration, + ); + query.query = await this.engine.getPartialQueryCode(currentSnapshot); + + let persistentQuery: PersistentDataCubeQuery; + if (existingPersistentQuery) { + persistentQuery = existingPersistentQuery.clone(); + } else { + persistentQuery = new PersistentDataCubeQuery(); + persistentQuery.id = uuid(); + } + persistentQuery.name = name; + persistentQuery.content = DataCubeQuery.serialization.toJson(query); + return persistentQuery; + } + + async createQuery(name: string) { + if (!this.builder?.dataCube || this.saveQueryState.isInProgress) { + return; + } + + this.saveQueryState.inProgress(); + try { + const persistentQuery = await this.generatePersistentQuery( + this.builder.dataCube, + name, + ); + + const newQuery = + await this.baseStore.graphManager.createDataCubeQuery(persistentQuery); + // NOTE: reload is the cleanest, least bug-prone handling here + // but we can opt for just updating the URL to reflect the new query + // as an optimization. Also, it helps preserve the edition history + // on the existing data-cube. + // + // Another way to avoid reloading the whole app it to force update + // the component using the key prop that ties to an ID + // of the builder. + this.application.navigationService.navigator.updateCurrentLocation( + generateQueryBuilderRoute(newQuery.id), + ); + this.builder.setPersistentQuery(persistentQuery); + this.updateWindowTitle(persistentQuery); + + this.saverDisplay.close(); + this.saveQueryState.pass(); + } catch (error) { + assertErrorThrown(error); + this.engine.alertError(error, { + message: `Query Creation Failure: ${error.message}`, + }); + this.saveQueryState.fail(); + } + } + + async saveQuery(name: string, saveAsNewQuery: boolean) { + if (!this.builder?.dataCube || this.saveQueryState.isInProgress) { + return; + } + + this.saveQueryState.inProgress(); + try { + const persistentQuery = await this.generatePersistentQuery( + this.builder.dataCube, + name, + this.builder.persistentQuery, + ); + + if (saveAsNewQuery) { + persistentQuery.id = uuid(); + const newQuery = + await this.baseStore.graphManager.createDataCubeQuery( + persistentQuery, + ); + // NOTE: reload is the cleanest, least bug-prone handling here + // but we can opt for just updating the URL to reflect the new query + // as an optimization. Also, it helps preserve the edition history + // on the existing data-cube. + // + // Another way to avoid reloading the whole app it to force update + // the component using the key prop that ties to an ID + // of the builder. + this.application.navigationService.navigator.updateCurrentLocation( + generateQueryBuilderRoute(newQuery.id), + ); + } else { + await this.baseStore.graphManager.updateDataCubeQuery(persistentQuery); + } + this.builder.setPersistentQuery(persistentQuery); + this.updateWindowTitle(persistentQuery); + + this.saverDisplay.close(); + this.saveQueryState.pass(); + } catch (error) { + assertErrorThrown(error); + this.engine.alertError(error, { + message: `Query Update Failure: ${error.message}`, + }); + this.saveQueryState.fail(); + } + } +} diff --git a/packages/legend-application-data-cube/src/stores/query-builder/source-builder/LegendDataCubeSourceBuilderState.ts b/packages/legend-application-data-cube/src/stores/query-builder/source-builder/LegendDataCubeSourceBuilderState.ts index eb0bd95d332..0610931de5d 100644 --- a/packages/legend-application-data-cube/src/stores/query-builder/source-builder/LegendDataCubeSourceBuilderState.ts +++ b/packages/legend-application-data-cube/src/stores/query-builder/source-builder/LegendDataCubeSourceBuilderState.ts @@ -15,11 +15,8 @@ */ import { ActionState, type PlainObject } from '@finos/legend-shared'; -import type { - LegendDataCubeApplicationStore, - LegendDataCubeBaseStore, -} from '../../LegendDataCubeBaseStore.js'; -import type { V1_PureGraphManager } from '@finos/legend-graph'; +import type { LegendDataCubeApplicationStore } from '../../LegendDataCubeBaseStore.js'; +import type { LegendDataCubeNewQueryState } from '../LegendDataCubeNewQueryState.js'; export enum LegendDataCubeSourceBuilderType { LEGEND_QUERY = 'Legend Query', @@ -28,15 +25,10 @@ export enum LegendDataCubeSourceBuilderType { export abstract class LegendDataCubeSourceBuilderState { readonly application: LegendDataCubeApplicationStore; - readonly baseStore: LegendDataCubeBaseStore; - readonly graphManager: V1_PureGraphManager; - readonly buildState = ActionState.create(); - constructor(baseStore: LegendDataCubeBaseStore) { - this.application = baseStore.application; - this.baseStore = baseStore; - this.graphManager = baseStore.graphManager; + constructor(newQueryState: LegendDataCubeNewQueryState) { + this.application = newQueryState.application; } abstract get label(): LegendDataCubeSourceBuilderType; diff --git a/packages/legend-application-data-cube/src/stores/query-builder/source-builder/LegendQueryDataCubeSourceBuilderState.ts b/packages/legend-application-data-cube/src/stores/query-builder/source-builder/LegendQueryDataCubeSourceBuilderState.ts index 5a36c27789e..f676502a669 100644 --- a/packages/legend-application-data-cube/src/stores/query-builder/source-builder/LegendQueryDataCubeSourceBuilderState.ts +++ b/packages/legend-application-data-cube/src/stores/query-builder/source-builder/LegendQueryDataCubeSourceBuilderState.ts @@ -25,16 +25,16 @@ import { LegendDataCubeSourceBuilderState, LegendDataCubeSourceBuilderType, } from './LegendDataCubeSourceBuilderState.js'; -import type { LegendDataCubeBaseStore } from '../../LegendDataCubeBaseStore.js'; import { RawLegendQueryDataCubeSource } from '../../model/LegendQueryDataCubeSource.js'; +import type { LegendDataCubeNewQueryState } from '../LegendDataCubeNewQueryState.js'; export class LegendQueryDataCubeSourceBuilderState extends LegendDataCubeSourceBuilderState { readonly queryLoaderState: QueryLoaderState; query?: LightQuery | undefined; - constructor(baseStore: LegendDataCubeBaseStore) { - super(baseStore); + constructor(newQueryState: LegendDataCubeNewQueryState) { + super(newQueryState); makeObservable(this, { query: observable, @@ -43,7 +43,7 @@ export class LegendQueryDataCubeSourceBuilderState extends LegendDataCubeSourceB this.queryLoaderState = new QueryLoaderState( this.application, - this.graphManager, + newQueryState.engine.graphManager, { loadQuery: (query: LightQuery): void => { this.setQuery(query); @@ -52,7 +52,7 @@ export class LegendQueryDataCubeSourceBuilderState extends LegendDataCubeSourceB fetchDefaultQueries: async (): Promise => { const searchSpecification = new QuerySearchSpecification(); searchSpecification.limit = QUERY_LOADER_TYPEAHEAD_SEARCH_LIMIT; - return this.graphManager.searchQueries( + return newQueryState.engine.graphManager.searchQueries( QuerySearchSpecification.createDefault(undefined), ); }, diff --git a/packages/legend-application-repl/src/components/LegendREPLPublishDataCubeAlert.tsx b/packages/legend-application-repl/src/components/LegendREPLPublishDataCubeAlert.tsx index e5488dbc6d4..c6ef0d74ead 100644 --- a/packages/legend-application-repl/src/components/LegendREPLPublishDataCubeAlert.tsx +++ b/packages/legend-application-repl/src/components/LegendREPLPublishDataCubeAlert.tsx @@ -35,7 +35,7 @@ export const LegendREPLPublishDataCubeAlert = (props: {
- +
diff --git a/packages/legend-art/src/icon/DataCubeIcon.tsx b/packages/legend-art/src/icon/DataCubeIcon.tsx index a8720e820a4..c7a6942b7c9 100644 --- a/packages/legend-art/src/icon/DataCubeIcon.tsx +++ b/packages/legend-art/src/icon/DataCubeIcon.tsx @@ -78,12 +78,19 @@ import { TbCircleXFilled, TbClipboard, } from 'react-icons/tb'; -import { VscError, VscInfo, VscQuestion, VscWarning } from 'react-icons/vsc'; +import { + VscError, + VscInfo, + VscPass, + VscQuestion, + VscWarning, +} from 'react-icons/vsc'; export const DataCubeIcon = { AdvancedSettings: TbSettingsBolt, AlertError: VscError, AlertInfo: VscInfo, + AlertSuccess: VscPass, AlertWarning: VscWarning, Calculate: TbCalculator, Calendar: TbCalendarEvent, diff --git a/packages/legend-data-cube/src/components/core/DataCubeAlert.tsx b/packages/legend-data-cube/src/components/core/DataCubeAlert.tsx index 739101e8556..e1cc01e3563 100644 --- a/packages/legend-data-cube/src/components/core/DataCubeAlert.tsx +++ b/packages/legend-data-cube/src/components/core/DataCubeAlert.tsx @@ -22,9 +22,10 @@ import { useDataCube } from '../DataCubeProvider.js'; import { FormButton } from './DataCubeFormUtils.js'; export enum AlertType { + ERROR = 'ERROR', INFO = 'INFO', + SUCCESS = 'SUCCESS', WARNING = 'WARNING', - ERROR = 'ERROR', } export type ActionAlertAction = { label: string; handler: () => void }; @@ -57,15 +58,18 @@ export function Alert(props: { })} >
+ {type === AlertType.ERROR && ( + + )} {type === AlertType.INFO && ( )} + {type === AlertType.SUCCESS && ( + + )} {type === AlertType.WARNING && ( )} - {type === AlertType.ERROR && ( - - )}
{message}
diff --git a/packages/legend-data-cube/src/components/core/DataCubeFormUtils.tsx b/packages/legend-data-cube/src/components/core/DataCubeFormUtils.tsx index 2fcfbc6c676..3b8bc444f2e 100644 --- a/packages/legend-data-cube/src/components/core/DataCubeFormUtils.tsx +++ b/packages/legend-data-cube/src/components/core/DataCubeFormUtils.tsx @@ -476,7 +476,6 @@ function FormColorPicker(props: { onClick={() => { onClose(); }} - autoFocus={true} > Cancel diff --git a/packages/legend-data-cube/src/components/core/DataCubeLayoutManager.tsx b/packages/legend-data-cube/src/components/core/DataCubeLayoutManager.tsx index 3a1bdb51e1f..3231d01d58c 100644 --- a/packages/legend-data-cube/src/components/core/DataCubeLayoutManager.tsx +++ b/packages/legend-data-cube/src/components/core/DataCubeLayoutManager.tsx @@ -163,7 +163,7 @@ export const Window = (props: {
-
+
{windowState.configuration.contentRenderer(windowState.configuration)}
diff --git a/packages/legend-data-cube/src/components/view/editor/DataCubeEditor.tsx b/packages/legend-data-cube/src/components/view/editor/DataCubeEditor.tsx index 995074e093a..fe6b929e747 100644 --- a/packages/legend-data-cube/src/components/view/editor/DataCubeEditor.tsx +++ b/packages/legend-data-cube/src/components/view/editor/DataCubeEditor.tsx @@ -81,9 +81,7 @@ export const DataCubeEditor = observer((props: { view: DataCubeViewState }) => {
- editor.display.close()} autoFocus={true}> - Cancel - + editor.display.close()}>Cancel -
+
@@ -341,9 +341,7 @@ export const DataCubeColumnCreator = observer(
- state.close()} autoFocus={true}> - Cancel - + state.close()}>Cancel {state instanceof DataCubeExistingColumnEditorState && ( <>
- editor.display.close()} autoFocus={true}> - Cancel - + editor.display.close()}>Cancel editor.applyChanges()}> Apply diff --git a/packages/legend-data-cube/src/index.tsx b/packages/legend-data-cube/src/index.tsx index 6d19fdf075f..2ad0f84f1f1 100644 --- a/packages/legend-data-cube/src/index.tsx +++ b/packages/legend-data-cube/src/index.tsx @@ -32,10 +32,11 @@ export { type DataCubeSettingValues, } from './stores/core/DataCubeSetting.js'; export { DataCubeState } from './stores/DataCubeState.js'; +export { useDataCube } from './components/DataCubeProvider.js'; export * from './stores/DataCubeAPI.js'; export * from './stores/DataCubeOptions.js'; -export * from './components/core/DataCubeAlert.js'; export * from './components/DataCube.js'; +export * from './components/core/DataCubeAlert.js'; export * from './components/core/DataCubeFormUtils.js'; export * from './components/core/DataCubeLayoutManager.js'; diff --git a/packages/legend-data-cube/src/stores/DataCubeOptions.ts b/packages/legend-data-cube/src/stores/DataCubeOptions.ts index dd51892f608..9e646189285 100644 --- a/packages/legend-data-cube/src/stores/DataCubeOptions.ts +++ b/packages/legend-data-cube/src/stores/DataCubeOptions.ts @@ -24,6 +24,8 @@ import type { DataCubeState } from './DataCubeState.js'; export type DataCubeOptions = { gridClientLicense?: string | undefined; + onInitialized?: ((dataCube: DataCubeState) => void) | undefined; + onNameChanged?: ((name: string, source: DataCubeSource) => void) | undefined; innerHeaderComponent?: diff --git a/packages/legend-data-cube/src/stores/DataCubeState.tsx b/packages/legend-data-cube/src/stores/DataCubeState.tsx index e4b5f4c9509..87e3d325cac 100644 --- a/packages/legend-data-cube/src/stores/DataCubeState.tsx +++ b/packages/legend-data-cube/src/stores/DataCubeState.tsx @@ -46,6 +46,7 @@ export class DataCubeState implements DataCubeAPI { view: DataCubeViewState; readonly query: DataCubeQuery; + onInitialized?: ((dataCube: DataCubeState) => void) | undefined; onNameChanged?: ((name: string, source: DataCubeSource) => void) | undefined; innerHeaderComponent?: | ((dataCube: DataCubeState) => React.ReactNode) @@ -89,6 +90,7 @@ export class DataCubeState implements DataCubeAPI { }, ); + this.onInitialized = options?.onInitialized; this.onNameChanged = options?.onNameChanged; this.innerHeaderComponent = options?.innerHeaderComponent; } @@ -133,6 +135,8 @@ export class DataCubeState implements DataCubeAPI { await this.engine.initialize({ gridClientLicense: this.settings.gridClientLicense, }); + + this.onInitialized?.(this); this.initState.pass(); } catch (error) { assertErrorThrown(error); diff --git a/packages/legend-data-cube/src/stores/core/DataCubeQueryBuilder.ts b/packages/legend-data-cube/src/stores/core/DataCubeQueryBuilder.ts index 4f7f93f5fa3..5aea31ca497 100644 --- a/packages/legend-data-cube/src/stores/core/DataCubeQueryBuilder.ts +++ b/packages/legend-data-cube/src/stores/core/DataCubeQueryBuilder.ts @@ -79,6 +79,7 @@ export function buildExecutableQuery( end: number; } | undefined; + skipExecutionContext?: boolean; }, ) { const data = snapshot.data; diff --git a/packages/legend-extension-dsl-data-quality/src/graph-manager/protocol/pure/v1/V1_DSL_Data_Quality_PureGraphManagerExtension.ts b/packages/legend-extension-dsl-data-quality/src/graph-manager/protocol/pure/v1/V1_DSL_Data_Quality_PureGraphManagerExtension.ts index ddb69a7cf69..719c379d3ac 100644 --- a/packages/legend-extension-dsl-data-quality/src/graph-manager/protocol/pure/v1/V1_DSL_Data_Quality_PureGraphManagerExtension.ts +++ b/packages/legend-extension-dsl-data-quality/src/graph-manager/protocol/pure/v1/V1_DSL_Data_Quality_PureGraphManagerExtension.ts @@ -45,7 +45,6 @@ import { V1_parameterValueModelSchema, V1_transformParameterValue, V1_RemoteEngine, - V1_transformParameterValue, } from '@finos/legend-graph'; import { createModelSchema, optional, primitive } from 'serializr'; import { diff --git a/packages/legend-graph/src/graph-manager/action/query/PersistentDataCubeQuery.ts b/packages/legend-graph/src/graph-manager/action/query/PersistentDataCubeQuery.ts index 93b4b4f0324..eb751fa7fc0 100644 --- a/packages/legend-graph/src/graph-manager/action/query/PersistentDataCubeQuery.ts +++ b/packages/legend-graph/src/graph-manager/action/query/PersistentDataCubeQuery.ts @@ -40,6 +40,12 @@ export class PersistentDataCubeQuery { owner: optional(primitive()), }), ); + + clone() { + return PersistentDataCubeQuery.serialization.fromJson( + PersistentDataCubeQuery.serialization.toJson(this), + ); + } } export class LightPersistentDataCubeQuery {