From 2fa496bfce04a1bc42d75f2c31febc48a938bcbc Mon Sep 17 00:00:00 2001 From: Yannan <73408381+YannanGao-gs@users.noreply.github.com> Date: Wed, 30 Oct 2024 17:49:35 -0400 Subject: [PATCH] leverage minimal graph to build query builder with datsapaces (#3488) --- .changeset/kind-rabbits-draw.md | 6 + .changeset/twenty-buses-sit.md | 7 + .../LegendQueryApplicationConfig.ts | 3 + .../Core_LegendQueryApplicationPlugin.tsx | 288 +++++++++++- .../src/components/QueryEditor.tsx | 26 +- .../QueryEditorComponentTestUtils.tsx | 86 +++- .../__tests__/QueryEditor.dataspace.test.tsx | 67 +++ .../stores/LegendQueryApplicationPlugin.tsx | 11 +- .../src/stores/QueryEditorStore.ts | 307 +++++++++--- .../data-space/DataSpaceQueryCreatorStore.ts | 45 +- .../data-space/DataSpaceQuerySetupState.ts | 1 + .../DataSpaceTemplateQueryCreatorStore.ts | 124 +++-- .../function-activator/FunctionEditor.tsx | 2 +- .../MappingExecutionBuilder.tsx | 2 +- .../mapping-editor/MappingTestableEditor.tsx | 2 +- .../legacy/DEPRECATED__MappingTestEditor.tsx | 4 +- .../ServiceExecutionQueryEditor.tsx | 4 +- .../uml-editor/ClassQueryBuilder.tsx | 2 +- .../editor/EmbeddedQueryBuilderState.ts | 5 +- .../database/QueryDatabaseState.ts | 2 +- ...ryConnectionEndToEndWorkflowEditorState.ts | 2 +- .../src/components/DataSpaceQueryAction.tsx | 13 +- .../DSL_DataSpace_LegendApplicationPlugin.tsx | 14 +- .../query-builder/DataSpaceQueryBuilder.tsx | 49 +- ...eQueryBuilderTemplateQueryPanelContent.tsx | 2 +- .../src/graph-manager/index.ts | 4 + ...DSL_DataSpace_PureGraphManagerExtension.ts | 20 +- ...DSL_DataSpace_PureGraphManagerExtension.ts | 438 ++++++++++++++---- .../DataSpaceQueryBuilderState.ts | 72 ++- .../graph-manager/GraphManagerStatistics.ts | 1 + .../analytics/MappingModelCoverageAnalysis.ts | 31 +- .../src/graph-manager/action/query/Query.ts | 4 +- .../protocol/pure/v1/V1_PureGraphManager.ts | 75 ++- .../pure/v1/engine/V1_EngineHelper.ts | 6 +- .../pure/v1/engine/V1_GraphManagerEngine.ts | 2 + .../V1_MappingModelCoverageAnalysis.ts | 60 ++- packages/legend-graph/src/index.ts | 2 + .../src/components/QueryLoader.tsx | 10 +- .../workflows/ServiceQueryBuilder.tsx | 14 +- .../src/stores/QueryBuilderState.ts | 23 +- ...ilder_LegendApplicationPlugin_Extension.ts | 12 +- .../workflows/ServiceQueryBuilderState.ts | 36 +- 42 files changed, 1480 insertions(+), 404 deletions(-) create mode 100644 .changeset/kind-rabbits-draw.md create mode 100644 .changeset/twenty-buses-sit.md diff --git a/.changeset/kind-rabbits-draw.md b/.changeset/kind-rabbits-draw.md new file mode 100644 index 0000000000..457d23317c --- /dev/null +++ b/.changeset/kind-rabbits-draw.md @@ -0,0 +1,6 @@ +--- +'@finos/legend-extension-dsl-data-space': minor +'@finos/legend-application-query': minor +--- + +Use PMCD returned by mapping analysis to build minimal graph for query diff --git a/.changeset/twenty-buses-sit.md b/.changeset/twenty-buses-sit.md new file mode 100644 index 0000000000..5d65d03773 --- /dev/null +++ b/.changeset/twenty-buses-sit.md @@ -0,0 +1,7 @@ +--- +'@finos/legend-extension-dsl-data-space-studio': patch +'@finos/legend-application-studio': patch +'@finos/legend-query-builder': patch +'@finos/legend-server-depot': patch +'@finos/legend-graph': patch +--- diff --git a/packages/legend-application-query/src/application/LegendQueryApplicationConfig.ts b/packages/legend-application-query/src/application/LegendQueryApplicationConfig.ts index 7ac98de41f..766fa1f2c6 100644 --- a/packages/legend-application-query/src/application/LegendQueryApplicationConfig.ts +++ b/packages/legend-application-query/src/application/LegendQueryApplicationConfig.ts @@ -65,6 +65,8 @@ class LegendQueryApplicationCoreOptions { TEMPORARY__serviceRegistrationConfig: ServiceRegistrationEnvironmentConfig[] = []; + TEMPORARY__enableMinimalGraph = false; + /** * Config specific to query builder */ @@ -78,6 +80,7 @@ class LegendQueryApplicationCoreOptions { queryBuilderConfig: optional( usingModelSchema(QueryBuilderConfig.serialization.schema), ), + TEMPORARY__enableMinimalGraph: optional(primitive()), }), ); diff --git a/packages/legend-application-query/src/components/Core_LegendQueryApplicationPlugin.tsx b/packages/legend-application-query/src/components/Core_LegendQueryApplicationPlugin.tsx index 00f9e5404c..cf1c1c064c 100644 --- a/packages/legend-application-query/src/components/Core_LegendQueryApplicationPlugin.tsx +++ b/packages/legend-application-query/src/components/Core_LegendQueryApplicationPlugin.tsx @@ -15,9 +15,9 @@ */ import { + type QuerySetupActionConfiguration, LegendQueryApplicationPlugin, QuerySetupActionTag, - type QuerySetupActionConfiguration, } from '../stores/LegendQueryApplicationPlugin.js'; import packageJson from '../../package.json' with { type: 'json' }; import type { QuerySetupLandingPageStore } from '../stores/QuerySetupStore.js'; @@ -50,10 +50,10 @@ import { LEGEND_QUERY_ROUTE_PATTERN, } from '../__lib__/LegendQueryNavigation.js'; import { - ActionAlertActionType, - ActionAlertType, type ApplicationPageEntry, type LegendApplicationSetup, + ActionAlertActionType, + ActionAlertType, } from '@finos/legend-application'; import { CloneQueryServiceSetup } from './CloneQueryServiceSetup.js'; import { QueryProductionizerSetup } from './QueryProductionizerSetup.js'; @@ -65,9 +65,11 @@ import { generateDataSpaceQuerySetupRoute, } from '../__lib__/DSL_DataSpace_LegendQueryNavigation.js'; import { - QUERY_BUILDER_SUPPORTED_GET_ALL_FUNCTIONS, + type QueryBuilderState, type QueryBuilderHeaderActionConfiguration, type QueryBuilderMenuActionConfiguration, + type QueryBuilderPropagateExecutionContextChangeHelper, + QUERY_BUILDER_SUPPORTED_GET_ALL_FUNCTIONS, } from '@finos/legend-query-builder'; import { ExistingQueryEditorStore, @@ -75,18 +77,48 @@ import { } from '../stores/QueryEditorStore.js'; import { DataSpaceQueryBuilderState, + DataSpacesDepotRepository, generateDataSpaceTemplateQueryPromotionRoute, } from '@finos/legend-extension-dsl-data-space/application'; -import { RuntimePointer } from '@finos/legend-graph'; +import { + createGraphBuilderReport, + GRAPH_MANAGER_EVENT, + LegendSDLC, + PackageableElementPointerType, + resolvePackagePathAndElementName, + RuntimePointer, + V1_EngineRuntime, + V1_Mapping, + V1_PackageableElementPointer, + V1_PackageableRuntime, + V1_PureGraphManager, +} from '@finos/legend-graph'; import { LegendQueryTelemetryHelper } from '../__lib__/LegendQueryTelemetryHelper.js'; -import { StoreProjectData } from '@finos/legend-server-depot'; -import { buildUrl } from '@finos/legend-shared'; +import { resolveVersion, StoreProjectData } from '@finos/legend-server-depot'; +import { + ActionState, + assertErrorThrown, + buildUrl, + getNullableFirstEntry, + guaranteeNonNullable, + guaranteeType, + LogEvent, + StopWatch, + uniq, +} from '@finos/legend-shared'; import { parseProjectIdentifier } from '@finos/legend-storage'; import { QueryEditorExistingQueryHeader } from './QueryEditor.js'; import { DataSpaceTemplateQueryCreatorStore } from '../stores/data-space/DataSpaceTemplateQueryCreatorStore.js'; import { createViewSDLCProjectHandler } from '../stores/data-space/DataSpaceQueryBuilderHelper.js'; import { DataSpaceQueryCreatorStore } from '../stores/data-space/DataSpaceQueryCreatorStore.js'; import { configureCodeEditorComponent } from '@finos/legend-lego/code-editor'; +import { + resolveUsableDataSpaceClasses, + V1_DataSpace, + V1_DataSpaceExecutionContext, +} from '@finos/legend-extension-dsl-data-space/graph'; +import { flowResult } from 'mobx'; +import { LEGEND_QUERY_APP_EVENT } from '../__lib__/LegendQueryEvent.js'; export class Core_LegendQueryApplicationPlugin extends LegendQueryApplicationPlugin { static NAME = packageJson.extensions.applicationQueryPlugin; @@ -725,4 +757,246 @@ export class Core_LegendQueryApplicationPlugin extends LegendQueryApplicationPlu }, }; } + + getExtraQueryBuilderPropagateExecutionContextChangeHelper?(): QueryBuilderPropagateExecutionContextChangeHelper[] { + return [ + ( + queryBuilderState: QueryBuilderState, + isGraphBuildingNotRequired?: boolean, + ): (() => Promise) | undefined => { + /** + * Propagation after changing the execution context: + * - The mapping will be updated to the mapping of the execution context + * - The runtime will be updated to the default runtime of the execution context + * - If no class is chosen, try to choose a compatible class + * - If the chosen class is compatible with the new selected execution context mapping, do nothing, otherwise, try to choose a compatible class + */ + const propagateExecutionContextChange = async (): Promise => { + const dataSpaceQueryBuilderState = guaranteeType( + queryBuilderState, + DataSpaceQueryBuilderState, + ); + const mapping = + dataSpaceQueryBuilderState.executionContext.mapping.value; + const mappingModelCoverageAnalysisResult = + dataSpaceQueryBuilderState.dataSpaceAnalysisResult?.mappingToMappingCoverageResult?.get( + mapping.path, + ); + const editorStore = ( + queryBuilderState.workflowState + .actionConfig as QueryBuilderActionConfig_QueryApplication + ).editorStore; + if ( + dataSpaceQueryBuilderState.dataSpaceAnalysisResult && + mappingModelCoverageAnalysisResult + ) { + if ( + !isGraphBuildingNotRequired && + dataSpaceQueryBuilderState.isLightGraphEnabled + ) { + const supportBuildMinimalGraph = + editorStore.applicationStore.config.options + .TEMPORARY__enableMinimalGraph; + if ( + editorStore.enableMinialGraphForDataSpaceLoadingPerformance && + supportBuildMinimalGraph + ) { + try { + const stopWatch = new StopWatch(); + const graph = + dataSpaceQueryBuilderState.graphManagerState.createNewGraph(); + const graph_buildReport = createGraphBuilderReport(); + const graphManager = guaranteeType( + dataSpaceQueryBuilderState.graphManagerState.graphManager, + V1_PureGraphManager, + ); + // Create dummy mappings and runtimes + // TODO?: these stubbed mappings and runtimes are not really useful that useful, so either we should + // simplify the model here or potentially refactor the backend analytics endpoint to return these as model + const mappingModels = uniq( + Array.from( + dataSpaceQueryBuilderState.dataSpaceAnalysisResult.executionContextsIndex.values(), + ).map((context) => context.mapping), + ).map((m) => { + const _mapping = new V1_Mapping(); + const [packagePath, name] = + resolvePackagePathAndElementName(m.path); + _mapping.package = packagePath; + _mapping.name = name; + return graphManager.elementProtocolToEntity(_mapping); + }); + const runtimeModels = uniq( + Array.from( + dataSpaceQueryBuilderState.dataSpaceAnalysisResult.executionContextsIndex.values(), + ) + .map((context) => context.defaultRuntime) + .concat( + Array.from( + dataSpaceQueryBuilderState.dataSpaceAnalysisResult.executionContextsIndex.values(), + ).flatMap((val) => val.compatibleRuntimes), + ), + ).map((r) => { + const runtime = new V1_PackageableRuntime(); + const [packagePath, name] = + resolvePackagePathAndElementName(r.path); + runtime.package = packagePath; + runtime.name = name; + runtime.runtimeValue = new V1_EngineRuntime(); + return graphManager.elementProtocolToEntity(runtime); + }); + // The DataSpace entity is excluded from AnalyticsResult.Json to reduce the JSON size + // because all its information can be found in V1_DataSpaceAnalysisResult. + // Therefore, we are building a simple v1_DataSpace entity based on V1_DataSpaceAnalysisResult. + const dataspaceProtocol = new V1_DataSpace(); + dataspaceProtocol.name = + dataSpaceQueryBuilderState.dataSpaceAnalysisResult.name; + dataspaceProtocol.package = + dataSpaceQueryBuilderState.dataSpaceAnalysisResult.package; + dataspaceProtocol.supportInfo = + dataSpaceQueryBuilderState.dataSpaceAnalysisResult.supportInfo; + dataspaceProtocol.executionContexts = Array.from( + dataSpaceQueryBuilderState.dataSpaceAnalysisResult + .executionContextsIndex, + ).map(([key, execContext]) => { + const contextProtocol = new V1_DataSpaceExecutionContext(); + contextProtocol.name = execContext.name; + contextProtocol.title = execContext.title; + contextProtocol.description = execContext.description; + contextProtocol.mapping = new V1_PackageableElementPointer( + PackageableElementPointerType.MAPPING, + execContext.mapping.path, + ); + contextProtocol.defaultRuntime = + new V1_PackageableElementPointer( + PackageableElementPointerType.RUNTIME, + execContext.defaultRuntime.path, + ); + return contextProtocol; + }); + dataspaceProtocol.defaultExecutionContext = + dataSpaceQueryBuilderState.dataSpaceAnalysisResult.defaultExecutionContext.name; + dataspaceProtocol.title = + dataSpaceQueryBuilderState.dataSpaceAnalysisResult.title; + dataspaceProtocol.description = + dataSpaceQueryBuilderState.dataSpaceAnalysisResult.description; + const dataspaceEntity = + graphManager.elementProtocolToEntity(dataspaceProtocol); + + const graphEntities = guaranteeNonNullable( + mappingModelCoverageAnalysisResult.entities, + ) + .concat(mappingModels) + .concat(runtimeModels) + .concat(dataspaceEntity) + // NOTE: if an element could be found in the graph already it means it comes from system + // so we could rid of it + .filter( + (el) => + !graph.getNullableElement(el.path, false) && + !el.path.startsWith('meta::'), + ); + let option; + if ( + dataSpaceQueryBuilderState.dataSpaceRepo instanceof + DataSpacesDepotRepository + ) { + option = new LegendSDLC( + dataSpaceQueryBuilderState.dataSpaceRepo.project.groupId, + dataSpaceQueryBuilderState.dataSpaceRepo.project.artifactId, + resolveVersion( + dataSpaceQueryBuilderState.dataSpaceRepo.project + .versionId, + ), + ); + } + await dataSpaceQueryBuilderState.graphManagerState.graphManager.buildGraph( + graph, + graphEntities, + ActionState.create(), + option + ? { + origin: option, + } + : {}, + graph_buildReport, + ); + dataSpaceQueryBuilderState.graphManagerState.graph = graph; + const dependency_buildReport = createGraphBuilderReport(); + // report + stopWatch.record( + GRAPH_MANAGER_EVENT.INITIALIZE_GRAPH__SUCCESS, + ); + const graphBuilderReportData = { + timings: + dataSpaceQueryBuilderState.applicationStore.timeService.finalizeTimingsRecord( + stopWatch, + ), + dependencies: dependency_buildReport, + dependenciesCount: + dataSpaceQueryBuilderState.graphManagerState.graph + .dependencyManager.numberOfDependencies, + graph: graph_buildReport, + }; + editorStore.logBuildGraphMetrics(graphBuilderReportData); + dataSpaceQueryBuilderState.applicationStore.logService.info( + LogEvent.create( + GRAPH_MANAGER_EVENT.INITIALIZE_GRAPH__SUCCESS, + ), + graphBuilderReportData, + ); + } catch (error) { + assertErrorThrown(error); + editorStore.applicationStore.logService.error( + LogEvent.create(LEGEND_QUERY_APP_EVENT.GENERIC_FAILURE), + error, + ); + editorStore.graphManagerState.graph = + editorStore.graphManagerState.createNewGraph(); + await flowResult(editorStore.buildFullGraph()); + } + } else { + editorStore.graphManagerState.graph = + editorStore.graphManagerState.createNewGraph(); + await flowResult(editorStore.buildFullGraph()); + } + } + dataSpaceQueryBuilderState.explorerState.mappingModelCoverageAnalysisResult = + mappingModelCoverageAnalysisResult; + } + const compatibleClasses = resolveUsableDataSpaceClasses( + dataSpaceQueryBuilderState.dataSpace, + mapping, + dataSpaceQueryBuilderState.graphManagerState, + dataSpaceQueryBuilderState, + ); + dataSpaceQueryBuilderState.changeMapping(mapping); + dataSpaceQueryBuilderState.changeRuntime( + new RuntimePointer( + dataSpaceQueryBuilderState.executionContext.defaultRuntime, + ), + ); + // if there is no chosen class or the chosen one is not compatible + // with the mapping then pick a compatible class if possible + if ( + !dataSpaceQueryBuilderState.class || + !compatibleClasses.includes(dataSpaceQueryBuilderState.class) + ) { + const possibleNewClass = getNullableFirstEntry(compatibleClasses); + if (possibleNewClass) { + dataSpaceQueryBuilderState.changeClass(possibleNewClass); + } + } + dataSpaceQueryBuilderState.explorerState.refreshTreeData(); + }; + if ( + queryBuilderState instanceof DataSpaceQueryBuilderState && + queryBuilderState.workflowState.actionConfig instanceof + QueryBuilderActionConfig_QueryApplication + ) { + return propagateExecutionContextChange; + } + return undefined; + }, + ]; + } } diff --git a/packages/legend-application-query/src/components/QueryEditor.tsx b/packages/legend-application-query/src/components/QueryEditor.tsx index fb304ab50f..62fb5f148a 100644 --- a/packages/legend-application-query/src/components/QueryEditor.tsx +++ b/packages/legend-application-query/src/components/QueryEditor.tsx @@ -605,6 +605,12 @@ export const QueryEditor = observer(() => { !engineConfig.useClientRequestPayloadCompression, ); + const toggleEnableMinialGraphForDataSpaceLoadingPerformance = (): void => { + editorStore.setEnableMinialGraphForDataSpaceLoadingPerformance( + !editorStore.enableMinialGraphForDataSpaceLoadingPerformance, + ); + }; + const TEMPORARY__toggleLightDarkMode = (): void => { applicationStore.layoutService.setColorTheme( applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled @@ -629,7 +635,11 @@ export const QueryEditor = observer(() => { applicationStore.alertUnhandledError, ); applicationStore.releaseNotesService.updateViewedVersion(); - }, [editorStore, applicationStore]); + }, [ + editorStore, + applicationStore, + editorStore.enableMinialGraphForDataSpaceLoadingPerformance, + ]); return (
@@ -676,6 +686,20 @@ export const QueryEditor = observer(() => { Compress request payload + + + {editorStore.enableMinialGraphForDataSpaceLoadingPerformance ? ( + + ) : null} + + + Enable minimal graph + + } > diff --git a/packages/legend-application-query/src/components/__test-utils__/QueryEditorComponentTestUtils.tsx b/packages/legend-application-query/src/components/__test-utils__/QueryEditorComponentTestUtils.tsx index 8328e49225..06db63bc96 100644 --- a/packages/legend-application-query/src/components/__test-utils__/QueryEditorComponentTestUtils.tsx +++ b/packages/legend-application-query/src/components/__test-utils__/QueryEditorComponentTestUtils.tsx @@ -25,12 +25,15 @@ import { createMock, createSpy } from '@finos/legend-shared/test'; import { type GraphManagerState, type RawMappingModelCoverageAnalysisResult, + type QueryInfo, Query, LightQuery, RawLambda, - PackageableElementExplicitReference, QueryExplicitExecutionContext, QueryDataSpaceExecutionContext, + PackageableElementExplicitReference, + QueryDataSpaceExecutionContextInfo, + QueryExplicitExecutionContextInfo, } from '@finos/legend-graph'; import { DepotServerClient } from '@finos/legend-server-depot'; import { @@ -104,7 +107,7 @@ export const TEST__provideMockedQueryEditorStore = (customization?: { export const TEST__setUpQueryEditor = async ( MOCK__editorStore: ExistingQueryEditorStore, - entities: Entity[], + entities: PlainObject[], lambda: RawLambda, mappingPath: string, runtimePath: string, @@ -142,7 +145,7 @@ export const TEST__setUpQueryEditor = async ( await graphManagerState.initializeSystem(); await graphManagerState.graphManager.buildGraph( graphManagerState.graph, - entities, + entities as unknown as Entity[], graphManagerState.graphBuildState, ); @@ -162,13 +165,36 @@ export const TEST__setUpQueryEditor = async ( ); query.executionContext = execContext; query.content = 'some content'; + + const execContextInfo = new QueryExplicitExecutionContextInfo(); + execContextInfo.mapping = mappingPath; + execContextInfo.runtime = runtimePath; + + const queryInfo: QueryInfo = { + name: TEST_QUERY_NAME, + id: TEST_QUERY_ID, + versionId: '0.0.0', + groupId: 'test.group', + artifactId: 'test-artifact', + executionContext: execContextInfo, + content: 'some content', + isCurrentUserQuery: true, + }; + createSpy( MOCK__editorStore.depotServerClient, 'getProject', ).mockResolvedValue(projectData); + createSpy( + MOCK__editorStore.depotServerClient, + 'getEntities', + ).mockResolvedValue(entities); createSpy(graphManagerState.graphManager, 'getLightQuery').mockResolvedValue( lightQuery, ); + createSpy(graphManagerState.graphManager, 'getQueryInfo').mockResolvedValue( + queryInfo, + ); createSpy( graphManagerState.graphManager, 'pureCodeToLambda', @@ -222,16 +248,22 @@ export const TEST__setUpQueryEditor = async ( export const TEST__setUpDataSpaceExistingQueryEditor = async ( MOCK__editorStore: ExistingQueryEditorStore, - v1_dataspaceAnalyticsResult: PlainObject, + V1_dataspaceAnalyticsResult: PlainObject, dataSpacePath: string, executionContext: string, lambda: RawLambda, mappingPath: string, - entities: Entity[], + entities: PlainObject[], + buildWithMinimalGraph = false, ): Promise<{ renderResult: RenderResult; queryBuilderState: QueryBuilderState; }> => { + if (buildWithMinimalGraph) { + MOCK__editorStore.applicationStore.config.options.TEMPORARY__enableMinimalGraph = + true; + } + const projectData = { id: 'test-id', groupId: 'test.group', @@ -259,11 +291,6 @@ export const TEST__setUpDataSpaceExistingQueryEditor = async ( }); await graphManagerState.initializeSystem(); - await graphManagerState.graphManager.buildGraph( - graphManagerState.graph, - entities, - graphManagerState.graphBuildState, - ); const query = new Query(); query.name = lightQuery.name; @@ -278,6 +305,22 @@ export const TEST__setUpDataSpaceExistingQueryEditor = async ( execContext.executionKey = executionContext; query.executionContext = execContext; query.content = 'some content'; + + const execContextInfo = new QueryDataSpaceExecutionContextInfo(); + execContextInfo.dataSpacePath = dataSpacePath; + execContextInfo.executionKey = executionContext; + + const queryInfo: QueryInfo = { + name: TEST_QUERY_NAME, + id: TEST_QUERY_ID, + versionId: '0.0.0', + groupId: 'test.group', + artifactId: 'test-artifact', + executionContext: execContextInfo, + content: 'some content', + isCurrentUserQuery: true, + }; + createSpy( MOCK__editorStore.depotServerClient, 'getProject', @@ -285,7 +328,7 @@ export const TEST__setUpDataSpaceExistingQueryEditor = async ( createSpy( MOCK__editorStore.depotServerClient, 'getEntities', - ).mockResolvedValue([]); + ).mockResolvedValue(buildWithMinimalGraph ? [] : entities); createSpy( MOCK__editorStore.depotServerClient, 'getIndexedDependencyEntities', @@ -294,6 +337,10 @@ export const TEST__setUpDataSpaceExistingQueryEditor = async ( MOCK__editorStore.depotServerClient, 'getEntitiesByClassifier', ).mockResolvedValue([]); + createSpy( + MOCK__editorStore.depotServerClient, + 'getGenerationContentByPath', + ).mockResolvedValue(JSON.stringify(V1_dataspaceAnalyticsResult)); createSpy(graphManagerState.graphManager, 'getLightQuery').mockResolvedValue( lightQuery, ); @@ -308,10 +355,9 @@ export const TEST__setUpDataSpaceExistingQueryEditor = async ( createSpy(graphManagerState.graphManager, 'getQuery').mockResolvedValue( query, ); - createSpy( - MOCK__editorStore.depotServerClient, - 'getGenerationContentByPath', - ).mockResolvedValue(''); + createSpy(graphManagerState.graphManager, 'getQueryInfo').mockResolvedValue( + queryInfo, + ); createSpy(graphManagerState.graphManager, 'surveyDatasets').mockResolvedValue( [], ); @@ -325,20 +371,12 @@ export const TEST__setUpDataSpaceExistingQueryEditor = async ( ); const dataspaceAnalyticsResult = await graphManagerExtension.buildDataSpaceAnalytics( - v1_dataspaceAnalyticsResult, + V1_dataspaceAnalyticsResult, graphManagerState.graphManager.pluginManager.getPureProtocolProcessorPlugins(), ); createSpy(graphManagerExtension, 'analyzeDataSpace').mockResolvedValue( dataspaceAnalyticsResult, ); - const mappingAnalyticsResult = - dataspaceAnalyticsResult.mappingToMappingCoverageResult?.get(mappingPath); - if (mappingAnalyticsResult) { - createSpy( - graphManagerState.graphManager, - 'analyzeMappingModelCoverage', - ).mockResolvedValue(mappingAnalyticsResult); - } MOCK__editorStore.buildGraph = createMock(); graphManagerState.graphManager.initialize = createMock(); diff --git a/packages/legend-application-query/src/components/__tests__/QueryEditor.dataspace.test.tsx b/packages/legend-application-query/src/components/__tests__/QueryEditor.dataspace.test.tsx index 5ad0644913..2419ccce73 100644 --- a/packages/legend-application-query/src/components/__tests__/QueryEditor.dataspace.test.tsx +++ b/packages/legend-application-query/src/components/__tests__/QueryEditor.dataspace.test.tsx @@ -106,3 +106,70 @@ test( // await waitFor(() => getByText(dataspaceViewModal, 'this is template with inline query')); }, ); + +test( + integrationTest( + 'Load Existing DataSpace Query with minimal graph in Query Editor', + ), + async () => { + const mockedQueryEditorStore = TEST__provideMockedQueryEditorStore({ + extraPlugins: [new DSL_DataSpace_LegendApplicationPlugin()], + extraPresets: [new DSL_DataSpace_GraphManagerPreset()], + }); + mockedQueryEditorStore.setExistingQueryName(TEST_QUERY_NAME); + const { renderResult, queryBuilderState } = + await TEST__setUpDataSpaceExistingQueryEditor( + mockedQueryEditorStore, + TEST_DATA__DSL_DataSpace_AnalyticsResult, + 'domain::COVIDDatapace', + 'dummyContext', + stub_RawLambda(), + 'mapping::CovidDataMapping', + [], + true, + ); + + const _class = 'domain::COVIDData'; + const _modelClass = + queryBuilderState.graphManagerState.graph.getClass(_class); + await act(async () => { + queryBuilderState.changeClass(_modelClass); + }); + const explorerPanel = await waitFor(() => + renderResult.getByTestId(QUERY_BUILDER_TEST_ID.QUERY_BUILDER_EXPLORER), + ); + await waitFor(() => getByText(explorerPanel, 'Cases')); + await waitFor(() => getByText(explorerPanel, 'Case Type')); + const templateQueryPanel = await waitFor(() => + renderResult.getByTestId( + QUERY_BUILDER_TEST_ID.QUERY_BUILDER_TEMPLATE_QUERY_PANE, + ), + ); + const templateQueryIndicator = await waitFor(() => + getByText(templateQueryPanel, /Templates\s*\(\s*3\s*\)/), + ); + fireEvent.click(templateQueryIndicator); + await waitFor(() => + renderResult.getByText('this is template with function pointer'), + ); + await waitFor(() => + renderResult.getByText('this is template with service'), + ); + await waitFor(() => + renderResult.getByText('this is template with inline query'), + ); + await act(async () => { + fireEvent.click(renderResult.getByTitle('See more options')); + }); + await act(async () => { + fireEvent.click(renderResult.getByText('About Dataspace')); + }); + const aboutDataSpaceModal = await waitFor(() => + renderResult.getByRole('dialog'), + ); + await waitFor(() => getByText(aboutDataSpaceModal, 'COVID Sample Data')); + await waitFor(() => getByText(aboutDataSpaceModal, 'dummyContext')); + await waitFor(() => getByText(aboutDataSpaceModal, 'CovidDataMapping')); + await waitFor(() => getByText(aboutDataSpaceModal, 'H2Runtime')); + }, +); diff --git a/packages/legend-application-query/src/stores/LegendQueryApplicationPlugin.tsx b/packages/legend-application-query/src/stores/LegendQueryApplicationPlugin.tsx index 02c1b5034c..d2da44ffb9 100644 --- a/packages/legend-application-query/src/stores/LegendQueryApplicationPlugin.tsx +++ b/packages/legend-application-query/src/stores/LegendQueryApplicationPlugin.tsx @@ -15,17 +15,18 @@ */ import { LegendApplicationPlugin } from '@finos/legend-application'; -import type { Query } from '@finos/legend-graph'; import type { - QueryBuilderState, QueryBuilder_LegendApplicationPlugin_Extension, + QueryBuilderState, } from '@finos/legend-query-builder'; +import type React from 'react'; import type { LegendQueryPluginManager } from '../application/LegendQueryPluginManager.js'; -import { - type ExistingQueryEditorStore, - type QueryEditorStore, +import type { + ExistingQueryEditorStore, + QueryEditorStore, } from './QueryEditorStore.js'; import type { QuerySetupLandingPageStore } from './QuerySetupStore.js'; +import type { Query } from '@finos/legend-graph'; export enum QuerySetupActionTag { PRODUCTIONIZATION = 'Productionization', diff --git a/packages/legend-application-query/src/stores/QueryEditorStore.ts b/packages/legend-application-query/src/stores/QueryEditorStore.ts index c3f7398378..5659ec6ae1 100644 --- a/packages/legend-application-query/src/stores/QueryEditorStore.ts +++ b/packages/legend-application-query/src/stores/QueryEditorStore.ts @@ -49,6 +49,7 @@ import { type ValueSpecification, type GraphInitializationReport, type PackageableRuntime, + type QueryInfo, GraphManagerState, Query, PureExecution, @@ -65,12 +66,15 @@ import { reportGraphAnalytics, cloneQueryStereotype, cloneQueryTaggedValue, - QueryDataSpaceExecutionContext, - QueryExplicitExecutionContext, QueryProjectCoordinates, buildLambdaVariableExpressions, VariableExpression, PrimitiveType, + CORE_PURE_PATH, + isValidFullPath, + QUERY_PROFILE_PATH, + QueryDataSpaceExecutionContextInfo, + QueryExplicitExecutionContextInfo, } from '@finos/legend-graph'; import { generateExistingQueryEditorRoute, @@ -90,6 +94,8 @@ import { StoreProjectData, LATEST_VERSION_ALIAS, VersionedProjectData, + retrieveProjectEntitiesWithDependencies, + retrieveProjectEntitiesWithClassifier, } from '@finos/legend-server-depot'; import { ActionAlertActionType, @@ -119,10 +125,12 @@ import { type DataSpaceInfo, } from '@finos/legend-extension-dsl-data-space/application'; import { - DSL_DataSpace_getGraphManagerExtension, type DataSpace, type DataSpaceExecutionContext, + type DataSpaceAnalysisResult, + DSL_DataSpace_getGraphManagerExtension, getOwnDataSpace, + QUERY_PROFILE_TAG_DATA_SPACE, retrieveAnalyticsResultCache, } from '@finos/legend-extension-dsl-data-space/graph'; import { generateDataSpaceQueryCreatorRoute } from '../__lib__/DSL_DataSpace_LegendQueryNavigation.js'; @@ -269,6 +277,7 @@ export abstract class QueryEditorStore { showRegisterServiceModal = false; showAppInfo = false; showDataspaceInfo = false; + enableMinialGraphForDataSpaceLoadingPerformance = true; constructor( applicationStore: LegendQueryApplicationStore, @@ -282,13 +291,16 @@ export abstract class QueryEditorStore { showAppInfo: observable, showDataspaceInfo: observable, queryBuilderState: observable, + enableMinialGraphForDataSpaceLoadingPerformance: observable, isPerformingBlockingAction: computed, setExistingQueryName: action, setShowRegisterServiceModal: action, setShowAppInfo: action, setShowDataspaceInfo: action, + setEnableMinialGraphForDataSpaceLoadingPerformance: action, initialize: flow, buildGraph: flow, + buildFullGraph: flow, searchExistingQueryName: flow, }); @@ -390,6 +402,10 @@ export abstract class QueryEditorStore { this.showRegisterServiceModal = val; } + setEnableMinialGraphForDataSpaceLoadingPerformance(val: boolean): void { + this.enableMinialGraphForDataSpaceLoadingPerformance = val; + } + get isPerformingBlockingAction(): boolean { return this.queryCreatorState.createQueryState.isInProgress; } @@ -480,7 +496,10 @@ export abstract class QueryEditorStore { ): QueryPersistConfiguration | undefined; *initialize(): GeneratorFn { - if (!this.initState.isInInitialState) { + if ( + !this.initState.isInInitialState && + this.enableMinialGraphForDataSpaceLoadingPerformance + ) { return; } @@ -563,7 +582,7 @@ export abstract class QueryEditorStore { ); } - *buildGraph(): GeneratorFn { + *buildFullGraph(): GeneratorFn { const stopWatch = new StopWatch(); const projectInfo = this.getProjectInfo(); @@ -653,6 +672,7 @@ export abstract class QueryEditorStore { dependenciesCount: this.graphManagerState.graph.dependencyManager.numberOfDependencies, graph: graph_buildReport, + isLightGraphEnabled: false, }; this.logBuildGraphMetrics(graphBuilderReportData); @@ -662,6 +682,147 @@ export abstract class QueryEditorStore { ); } } + + *buildGraph(): GeneratorFn { + yield flowResult(this.buildFullGraph()); + } + + async buildGraphAndDataspaceAnalyticsResult( + groupId: string, + artifactId: string, + versionId: string, + executionContext: string | undefined, + dataSpacePath: string, + templateQueryId?: string | undefined, + ): Promise<{ + dataSpaceAnalysisResult: DataSpaceAnalysisResult | undefined; + isLightGraphEnabled: boolean; + }> { + let dataSpaceAnalysisResult; + let buildFullGraph = false; + let isLightGraphEnabled = true; + const supportBuildMinimalGraph = + this.applicationStore.config.options.TEMPORARY__enableMinimalGraph; + if ( + this.enableMinialGraphForDataSpaceLoadingPerformance && + supportBuildMinimalGraph + ) { + try { + this.initState.setMessage('Fetching dataspace analysis result...'); + const project = StoreProjectData.serialization.fromJson( + await this.depotServerClient.getProject(groupId, artifactId), + ); + const graph_buildReport = createGraphBuilderReport(); + const stopWatch = new StopWatch(); + // initialize system + stopWatch.record(); + await this.graphManagerState.initializeSystem(); + stopWatch.record(GRAPH_MANAGER_EVENT.INITIALIZE_GRAPH_SYSTEM__SUCCESS); + const dependency_buildReport = createGraphBuilderReport(); + dataSpaceAnalysisResult = await DSL_DataSpace_getGraphManagerExtension( + this.graphManagerState.graphManager, + ).analyzeDataSpaceCoverage( + dataSpacePath, + () => + retrieveProjectEntitiesWithDependencies( + project, + versionId, + this.depotServerClient, + ), + () => + retrieveProjectEntitiesWithClassifier( + project, + versionId, + CORE_PURE_PATH.FUNCTION, + this.depotServerClient, + ), + () => + retrieveAnalyticsResultCache( + project, + versionId, + dataSpacePath, + this.depotServerClient, + ), + undefined, + graph_buildReport, + this.graphManagerState.graph, + executionContext, + undefined, + this.getProjectInfo(), + templateQueryId, + ); + const mappingPath = executionContext + ? dataSpaceAnalysisResult.executionContextsIndex.get(executionContext) + ?.mapping.path + : undefined; + if (mappingPath) { + const pmcd = + dataSpaceAnalysisResult.mappingToMappingCoverageResult?.get( + mappingPath, + )?.entities; + if (pmcd) { + // report + stopWatch.record(GRAPH_MANAGER_EVENT.INITIALIZE_GRAPH__SUCCESS); + const graphBuilderReportData = { + timings: + this.applicationStore.timeService.finalizeTimingsRecord( + stopWatch, + ), + dependencies: dependency_buildReport, + dependenciesCount: + this.graphManagerState.graph.dependencyManager + .numberOfDependencies, + graph: graph_buildReport, + isLightGraphEnabled: true, + }; + this.logBuildGraphMetrics(graphBuilderReportData); + this.applicationStore.logService.info( + LogEvent.create(GRAPH_MANAGER_EVENT.INITIALIZE_GRAPH__SUCCESS), + graphBuilderReportData, + ); + } else { + buildFullGraph = true; + } + } + } catch (error) { + buildFullGraph = true; + this.applicationStore.logService.error( + LogEvent.create(LEGEND_QUERY_APP_EVENT.GENERIC_FAILURE), + error, + ); + } + } + if ( + !this.enableMinialGraphForDataSpaceLoadingPerformance || + buildFullGraph || + !supportBuildMinimalGraph + ) { + this.graphManagerState.graph = this.graphManagerState.createNewGraph(); + await flowResult(this.buildFullGraph()); + try { + const project = StoreProjectData.serialization.fromJson( + await this.depotServerClient.getProject(groupId, artifactId), + ); + dataSpaceAnalysisResult = await DSL_DataSpace_getGraphManagerExtension( + this.graphManagerState.graphManager, + ).retrieveDataSpaceAnalysisFromCache(() => + retrieveAnalyticsResultCache( + project, + versionId, + dataSpacePath, + this.depotServerClient, + ), + ); + } catch { + // do nothing + } + isLightGraphEnabled = false; + } + return { + dataSpaceAnalysisResult, + isLightGraphEnabled, + }; + } } export class QueryBuilderActionConfig_QueryApplication extends QueryBuilderActionConfig { @@ -1182,6 +1343,7 @@ export class ExistingQueryEditorStore extends QueryEditorStore { private queryId: string; private _lightQuery?: LightQuery | undefined; query: Query | undefined; + queryInfo: QueryInfo | undefined; urlQueryParamValues: Record | undefined; updateState: ExistingQueryUpdateState; @@ -1195,11 +1357,13 @@ export class ExistingQueryEditorStore extends QueryEditorStore { makeObservable(this, { query: observable, + queryInfo: observable, updateState: observable, _lightQuery: observable, lightQuery: computed, setLightQuery: action, setQuery: action, + setQueryInfo: action, isPerformingBlockingAction: override, }); @@ -1246,6 +1410,10 @@ export class ExistingQueryEditorStore extends QueryEditorStore { this.query = val; } + setQueryInfo(val: QueryInfo): void { + this.queryInfo = val; + } + getProjectInfo(): ProjectGAVCoordinates { return { groupId: this.lightQuery.groupId, @@ -1254,52 +1422,73 @@ export class ExistingQueryEditorStore extends QueryEditorStore { }; } + override *buildGraph(): GeneratorFn { + const queryInfo = this.queryInfo; + const dataSpaceTaggedValue = queryInfo?.taggedValues?.find( + (taggedValue) => + taggedValue.profile === QUERY_PROFILE_PATH && + taggedValue.tag === QUERY_PROFILE_TAG_DATA_SPACE && + isValidFullPath(taggedValue.value), + ); + if ( + !( + dataSpaceTaggedValue || + queryInfo?.executionContext instanceof + QueryDataSpaceExecutionContextInfo + ) + ) { + yield flowResult(this.buildFullGraph()); + } + } + override async setUpEditorState(): Promise { + const queryInfo = await this.graphManagerState.graphManager.getQueryInfo( + this.queryId, + ); this.setLightQuery( await this.graphManagerState.graphManager.getLightQuery(this.queryId), ); + this.setQueryInfo(queryInfo); + LegendQueryUserDataHelper.addRecentlyViewedQuery( + this.applicationStore.userDataService, + queryInfo.id, + ); } - async initQueryBuildStateFromQuery(query: Query): Promise { - const exec = query.executionContext; - if (exec instanceof QueryDataSpaceExecutionContext) { + async initQueryBuildStateFromQuery( + queryInfo: QueryInfo, + ): Promise { + const exec = queryInfo.executionContext; + if (exec instanceof QueryDataSpaceExecutionContextInfo) { + const { dataSpaceAnalysisResult, isLightGraphEnabled } = + await this.buildGraphAndDataspaceAnalyticsResult( + queryInfo.groupId, + queryInfo.artifactId, + queryInfo.versionId, + exec.executionKey, + exec.dataSpacePath, + ); const dataSpace = getOwnDataSpace( exec.dataSpacePath, this.graphManagerState.graph, ); + const mapping = queryInfo.mapping + ? this.graphManagerState.graph.getMapping(queryInfo.mapping) + : undefined; + const runtime = queryInfo.runtime + ? this.graphManagerState.graph.getRuntime(queryInfo.runtime) + : undefined; const matchingExecutionContext = resolveExecutionContext( dataSpace, exec.executionKey, - query.mapping?.value, - query.runtime?.value, + mapping, + runtime, ); if (matchingExecutionContext) { - let dataSpaceAnalysisResult; - try { - const project = StoreProjectData.serialization.fromJson( - await this.depotServerClient.getProject( - query.groupId, - query.artifactId, - ), - ); - dataSpaceAnalysisResult = - await DSL_DataSpace_getGraphManagerExtension( - this.graphManagerState.graphManager, - ).retrieveDataSpaceAnalysisFromCache(() => - retrieveAnalyticsResultCache( - project, - query.versionId, - dataSpace.path, - this.depotServerClient, - ), - ); - } catch { - // do nothing - } const sourceInfo = { - groupId: query.groupId, - artifactId: query.artifactId, - versionId: query.versionId, + groupId: queryInfo.groupId, + artifactId: queryInfo.artifactId, + versionId: queryInfo.versionId, dataSpace: dataSpace.path, }; const visitedDataSpaces = @@ -1313,15 +1502,16 @@ export class ExistingQueryEditorStore extends QueryEditorStore { new QueryBuilderActionConfig_QueryApplication(this), dataSpace, matchingExecutionContext, + isLightGraphEnabled, createDataSpaceDepoRepo( this, - query.groupId, - query.artifactId, - query.versionId, + queryInfo.groupId, + queryInfo.artifactId, + queryInfo.versionId, (dataSpaceInfo: DataSpaceInfo) => hasDataSpaceInfoBeenVisited(dataSpaceInfo, visitedDataSpaces), ), - (dataSpaceInfo: DataSpaceInfo) => { + async (dataSpaceInfo: DataSpaceInfo) => { if (dataSpaceInfo.defaultExecutionContext) { const proceed = (): void => this.applicationStore.navigationService.navigator.goToLocation( @@ -1351,7 +1541,7 @@ export class ExistingQueryEditorStore extends QueryEditorStore { } }; if ( - !query.isCurrentUserQuery || + !queryInfo.isCurrentUserQuery || !this.queryBuilderState?.changeDetectionState.hasChanged ) { proceed(); @@ -1415,7 +1605,7 @@ export class ExistingQueryEditorStore extends QueryEditorStore { `Unsupported execution context ${exec.executionKey}`, ); } - } else if (exec instanceof QueryExplicitExecutionContext) { + } else if (exec instanceof QueryExplicitExecutionContextInfo) { const projectInfo = this.getProjectInfo(); const sourceInfo = { groupId: projectInfo.groupId, @@ -1433,12 +1623,18 @@ export class ExistingQueryEditorStore extends QueryEditorStore { new QueryBuilderActionConfig_QueryApplication(this), ); classQueryBuilderState.executionContextState.setMapping( - exec.mapping.value, + exec.mapping + ? this.graphManagerState.graph.getMapping(exec.mapping) + : undefined, ); classQueryBuilderState.executionContextState.setRuntimeValue( - new RuntimePointer( - PackageableElementExplicitReference.create(exec.runtime.value), - ), + exec.runtime + ? new RuntimePointer( + PackageableElementExplicitReference.create( + this.graphManagerState.graph.getRuntime(exec.runtime), + ), + ) + : undefined, ); return classQueryBuilderState; } @@ -1448,6 +1644,19 @@ export class ExistingQueryEditorStore extends QueryEditorStore { async initializeQueryBuilderState( stopWatch: StopWatch, ): Promise { + // if no extension found, fall back to basic `class -> mapping -> runtime` mode + let queryInfo = this.queryInfo; + if (!queryInfo) { + queryInfo = await this.graphManagerState.graphManager.getQueryInfo( + this.queryId, + ); + } + const queryBuilderState = + await this.initQueryBuildStateFromQuery(queryInfo); + const initailizeQueryStateStopWatch = new StopWatch(); + const initailizeQueryStateReport = reportGraphAnalytics( + this.graphManagerState.graph, + ); const query = await this.graphManagerState.graphManager.getQuery( this.queryId, this.graphManagerState.graph, @@ -1457,14 +1666,6 @@ export class ExistingQueryEditorStore extends QueryEditorStore { this.applicationStore.userDataService, query.id, ); - - // if no extension found, fall back to basic `class -> mapping -> runtime` mode - const queryBuilderState = await this.initQueryBuildStateFromQuery(query); - - const initailizeQueryStateStopWatch = new StopWatch(); - const initailizeQueryStateReport = reportGraphAnalytics( - this.graphManagerState.graph, - ); const existingQueryLambda = await this.graphManagerState.graphManager.pureCodeToLambda(query.content); diff --git a/packages/legend-application-query/src/stores/data-space/DataSpaceQueryCreatorStore.ts b/packages/legend-application-query/src/stores/data-space/DataSpaceQueryCreatorStore.ts index 860ab04873..7c68caadc4 100644 --- a/packages/legend-application-query/src/stores/data-space/DataSpaceQueryCreatorStore.ts +++ b/packages/legend-application-query/src/stores/data-space/DataSpaceQueryCreatorStore.ts @@ -25,7 +25,6 @@ import { } from '@finos/legend-graph'; import { type DepotServerClient, - StoreProjectData, LATEST_VERSION_ALIAS, } from '@finos/legend-server-depot'; import { @@ -48,9 +47,7 @@ import { } from '@finos/legend-storage'; import { type DataSpaceExecutionContext, - DSL_DataSpace_getGraphManagerExtension, getDataSpace, - retrieveAnalyticsResultCache, } from '@finos/legend-extension-dsl-data-space/graph'; import { QueryBuilderActionConfig_QueryApplication, @@ -59,17 +56,17 @@ import { } from '../QueryEditorStore.js'; import type { LegendQueryApplicationStore } from '../LegendQueryBaseStore.js'; import { + type DataSpaceInfo, DataSpaceQueryBuilderState, createQueryClassTaggedValue, createQueryDataSpaceTaggedValue, - type DataSpaceInfo, } from '@finos/legend-extension-dsl-data-space/application'; import { LegendQueryUserDataHelper } from '../../__lib__/LegendQueryUserDataHelper.js'; import { + type VisitedDataspace, createVisitedDataSpaceId, hasDataSpaceInfoBeenVisited, createSimpleVisitedDataspace, - type VisitedDataspace, } from '../../__lib__/LegendQueryUserDataSpaceHelper.js'; import { LEGEND_QUERY_APP_EVENT } from '../../__lib__/LegendQueryEvent.js'; import { @@ -277,6 +274,14 @@ export class DataSpaceQueryCreatorStore extends QueryEditorStore { async initializeQueryBuilderStateWithQueryableDataSpace( queryableDataSpace: QueryableDataSpace, ): Promise { + const { dataSpaceAnalysisResult, isLightGraphEnabled } = + await this.buildGraphAndDataspaceAnalyticsResult( + queryableDataSpace.groupId, + queryableDataSpace.artifactId, + queryableDataSpace.versionId, + queryableDataSpace.executionContext, + queryableDataSpace.dataSpacePath, + ); const dataSpace = getDataSpace( queryableDataSpace.dataSpacePath, this.graphManagerState.graph, @@ -287,27 +292,6 @@ export class DataSpaceQueryCreatorStore extends QueryEditorStore { ), `Can't find execution context '${queryableDataSpace.executionContext}'`, ); - let dataSpaceAnalysisResult; - try { - const project = StoreProjectData.serialization.fromJson( - await this.depotServerClient.getProject( - queryableDataSpace.groupId, - queryableDataSpace.artifactId, - ), - ); - dataSpaceAnalysisResult = await DSL_DataSpace_getGraphManagerExtension( - this.graphManagerState.graphManager, - ).retrieveDataSpaceAnalysisFromCache(() => - retrieveAnalyticsResultCache( - project, - queryableDataSpace.versionId, - dataSpace.path, - this.depotServerClient, - ), - ); - } catch { - // do nothing - } const sourceInfo = { groupId: queryableDataSpace.groupId, artifactId: queryableDataSpace.artifactId, @@ -325,6 +309,7 @@ export class DataSpaceQueryCreatorStore extends QueryEditorStore { new QueryBuilderActionConfig_QueryApplication(this), dataSpace, executionContext, + isLightGraphEnabled, createDataSpaceDepoRepo( this, queryableDataSpace.groupId, @@ -333,7 +318,7 @@ export class DataSpaceQueryCreatorStore extends QueryEditorStore { (dataSpaceInfo: DataSpaceInfo) => hasDataSpaceInfoBeenVisited(dataSpaceInfo, visitedDataSpaces), ), - (dataSpaceInfo: DataSpaceInfo) => { + async (dataSpaceInfo: DataSpaceInfo) => { flowResult(this.changeDataSpace(dataSpaceInfo)).catch( this.applicationStore.alertUnhandledError, ); @@ -361,7 +346,7 @@ export class DataSpaceQueryCreatorStore extends QueryEditorStore { ); } queryBuilderState.setExecutionContext(executionContext); - queryBuilderState.propagateExecutionContextChange(executionContext); + await queryBuilderState.propagateExecutionContextChange(true); // set runtime if already chosen if (queryableDataSpace.runtimePath) { @@ -417,6 +402,10 @@ export class DataSpaceQueryCreatorStore extends QueryEditorStore { } } + override *buildGraph(): GeneratorFn { + // do nothing + } + addVisitedDataSpace(queryableDataSpace: QueryableDataSpace): void { try { LegendQueryUserDataHelper.addVisitedDatspace( diff --git a/packages/legend-application-query/src/stores/data-space/DataSpaceQuerySetupState.ts b/packages/legend-application-query/src/stores/data-space/DataSpaceQuerySetupState.ts index 0e79c4293b..a962737a93 100644 --- a/packages/legend-application-query/src/stores/data-space/DataSpaceQuerySetupState.ts +++ b/packages/legend-application-query/src/stores/data-space/DataSpaceQuerySetupState.ts @@ -67,6 +67,7 @@ export class DataSpaceQuerySetupState extends QueryBuilderState { dataSpaces: DataSpaceInfo[] = []; showRuntimeSelector = false; advancedSearchState?: DataSpaceAdvancedSearchState | undefined; + isLightGraphEnabled!: boolean; constructor( editorStore: QueryEditorStore, diff --git a/packages/legend-application-query/src/stores/data-space/DataSpaceTemplateQueryCreatorStore.ts b/packages/legend-application-query/src/stores/data-space/DataSpaceTemplateQueryCreatorStore.ts index 0e625799a3..400b27e891 100644 --- a/packages/legend-application-query/src/stores/data-space/DataSpaceTemplateQueryCreatorStore.ts +++ b/packages/legend-application-query/src/stores/data-space/DataSpaceTemplateQueryCreatorStore.ts @@ -21,11 +21,12 @@ import { QueryProjectCoordinates, extractElementNameFromPath, } from '@finos/legend-graph'; +import { type DepotServerClient } from '@finos/legend-server-depot'; import { - type DepotServerClient, - StoreProjectData, -} from '@finos/legend-server-depot'; -import { IllegalStateError, uuid } from '@finos/legend-shared'; + IllegalStateError, + uuid, + type GeneratorFn, +} from '@finos/legend-shared'; import { type QueryBuilderState, QueryBuilderDataBrowserWorkflow, @@ -35,18 +36,16 @@ import { parseGACoordinates, } from '@finos/legend-storage'; import { + type QueryPersistConfiguration, QueryBuilderActionConfig_QueryApplication, QueryEditorStore, - type QueryPersistConfiguration, } from '../QueryEditorStore.js'; import type { LegendQueryApplicationStore } from '../LegendQueryBaseStore.js'; import { - DSL_DataSpace_getGraphManagerExtension, + DataSpacePackageableElementExecutable, getDataSpace, - retrieveAnalyticsResultCache, getExecutionContextFromDataspaceExecutable, getQueryFromDataspaceExecutable, - DataSpacePackageableElementExecutable, } from '@finos/legend-extension-dsl-data-space/graph'; import { type DataSpaceInfo, @@ -90,62 +89,84 @@ export class DataSpaceTemplateQueryCreatorStore extends QueryEditorStore { }; } + override *buildGraph(): GeneratorFn { + // do nothing + } + async initializeQueryBuilderState(): Promise { + const { dataSpaceAnalysisResult, isLightGraphEnabled } = + await this.buildGraphAndDataspaceAnalyticsResult( + this.groupId, + this.artifactId, + this.versionId, + undefined, + this.dataSpacePath, + this.templateQueryId, + ); const dataSpace = getDataSpace( this.dataSpacePath, this.graphManagerState.graph, ); - let template = dataSpace.executables?.find( - (executable) => executable.id === this.templateQueryId, - ); - if (!template) { - template = dataSpace.executables?.find( - (executable) => - executable instanceof DataSpacePackageableElementExecutable && - executable.executable.value.path === this.templateQueryId, + let query; + let executionContext; + if (dataSpace.executables && dataSpace.executables.length > 0) { + let template = dataSpace.executables.find( + (ex) => ex.id === this.templateQueryId, ); - } - if (!template) { - throw new IllegalStateError( - `Can't find template query with id '${this.templateQueryId}'`, + if (!template) { + template = dataSpace.executables.find( + (executable) => + executable instanceof DataSpacePackageableElementExecutable && + executable.executable.value.path === this.templateQueryId, + ); + } + if (!template) { + throw new IllegalStateError( + `Can't find template query with id '${this.templateQueryId}'`, + ); + } + executionContext = getExecutionContextFromDataspaceExecutable( + dataSpace, + template, ); - } - const executionContext = getExecutionContextFromDataspaceExecutable( - dataSpace, - template, - ); - if (!executionContext) { - throw new IllegalStateError( - `Can't find a correpsonding execution context`, + query = getQueryFromDataspaceExecutable(template, this.graphManagerState); + this.templateQueryTitle = template.title; + } else { + let template = dataSpaceAnalysisResult?.executables.find( + (executable) => executable.info?.id === this.templateQueryId, ); + if (!template) { + template = dataSpaceAnalysisResult?.executables.find( + (executable) => executable.executable === this.templateQueryId, + ); + } + if (!template) { + throw new IllegalStateError( + `Can't find template query with id '${this.templateQueryId}'`, + ); + } + executionContext = + template.info?.executionContextKey === undefined + ? dataSpace.defaultExecutionContext + : dataSpace.executionContexts.find( + (ex) => ex.name === template.info?.executionContextKey, + ); + if (template.info) { + query = await this.graphManagerState.graphManager.pureCodeToLambda( + template.info.query, + ); + } + this.templateQueryTitle = template.title; } - const query = getQueryFromDataspaceExecutable( - template, - this.graphManagerState, - ); if (!query) { throw new IllegalStateError( `Can't fetch query from dataspace executable`, ); } - this.templateQueryTitle = template.title; - let dataSpaceAnalysisResult; - try { - const project = StoreProjectData.serialization.fromJson( - await this.depotServerClient.getProject(this.groupId, this.artifactId), - ); - dataSpaceAnalysisResult = await DSL_DataSpace_getGraphManagerExtension( - this.graphManagerState.graphManager, - ).retrieveDataSpaceAnalysisFromCache(() => - retrieveAnalyticsResultCache( - project, - this.versionId, - dataSpace.path, - this.depotServerClient, - ), + if (!executionContext) { + throw new IllegalStateError( + `Can't find a correpsonding execution context`, ); - } catch { - // do nothing } const sourceInfo = { groupId: this.groupId, @@ -160,6 +181,7 @@ export class DataSpaceTemplateQueryCreatorStore extends QueryEditorStore { new QueryBuilderActionConfig_QueryApplication(this), dataSpace, executionContext, + isLightGraphEnabled, createDataSpaceDepoRepo( this, this.groupId, @@ -167,7 +189,7 @@ export class DataSpaceTemplateQueryCreatorStore extends QueryEditorStore { this.versionId, undefined, ), - (dataSpaceInfo: DataSpaceInfo) => { + async (dataSpaceInfo: DataSpaceInfo) => { this.applicationStore.notificationService.notifyWarning( `Can't switch data space to visit current template query`, ); @@ -180,7 +202,7 @@ export class DataSpaceTemplateQueryCreatorStore extends QueryEditorStore { sourceInfo, ); queryBuilderState.setExecutionContext(executionContext); - queryBuilderState.propagateExecutionContextChange(executionContext); + await queryBuilderState.propagateExecutionContextChange(true); queryBuilderState.initializeWithQuery(query); return queryBuilderState; } diff --git a/packages/legend-application-studio/src/components/editor/editor-group/function-activator/FunctionEditor.tsx b/packages/legend-application-studio/src/components/editor/editor-group/function-activator/FunctionEditor.tsx index 136e945d78..502c3e963f 100644 --- a/packages/legend-application-studio/src/components/editor/editor-group/function-activator/FunctionEditor.tsx +++ b/packages/legend-application-studio/src/components/editor/editor-group/function-activator/FunctionEditor.tsx @@ -1239,7 +1239,7 @@ export const FunctionEditor = observer(() => { ); await flowResult( embeddedQueryBuilderState.setEmbeddedQueryBuilderConfiguration({ - setupQueryBuilderState: (): QueryBuilderState => + setupQueryBuilderState: async (): Promise => functionQueryBuilderState, actionConfigs: [ { diff --git a/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/MappingExecutionBuilder.tsx b/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/MappingExecutionBuilder.tsx index fd5971a0c6..f4d33036e6 100644 --- a/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/MappingExecutionBuilder.tsx +++ b/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/MappingExecutionBuilder.tsx @@ -207,7 +207,7 @@ const MappingExecutionQueryEditor = observer( const embeddedQueryBuilderState = editorStore.embeddedQueryBuilderState; await flowResult( embeddedQueryBuilderState.setEmbeddedQueryBuilderConfiguration({ - setupQueryBuilderState: (): QueryBuilderState => { + setupQueryBuilderState: async (): Promise => { const queryBuilderState = new MappingExecutionQueryBuilderState( embeddedQueryBuilderState.editorStore.applicationStore, embeddedQueryBuilderState.editorStore.graphManagerState, diff --git a/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/MappingTestableEditor.tsx b/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/MappingTestableEditor.tsx index f9bd787af5..f97d1c15b8 100644 --- a/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/MappingTestableEditor.tsx +++ b/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/MappingTestableEditor.tsx @@ -624,7 +624,7 @@ const MappingTestSuiteQueryEditor = observer( const embeddedQueryBuilderState = editorStore.embeddedQueryBuilderState; await flowResult( embeddedQueryBuilderState.setEmbeddedQueryBuilderConfiguration({ - setupQueryBuilderState: (): QueryBuilderState => { + setupQueryBuilderState: async (): Promise => { const queryBuilderState = new MappingExecutionQueryBuilderState( embeddedQueryBuilderState.editorStore.applicationStore, embeddedQueryBuilderState.editorStore.graphManagerState, diff --git a/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/legacy/DEPRECATED__MappingTestEditor.tsx b/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/legacy/DEPRECATED__MappingTestEditor.tsx index 83295913ad..f06365a347 100644 --- a/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/legacy/DEPRECATED__MappingTestEditor.tsx +++ b/packages/legend-application-studio/src/components/editor/editor-group/mapping-editor/legacy/DEPRECATED__MappingTestEditor.tsx @@ -17,7 +17,7 @@ import { useState, useEffect, useCallback } from 'react'; import { observer } from 'mobx-react-lite'; import { - type DEPRECATED__MappingTestState as DEPRECATED__MappingTestState, + type DEPRECATED__MappingTestState, MAPPING_TEST_EDITOR_TAB_TYPE, TEST_RESULT, MappingTestObjectInputDataState, @@ -109,7 +109,7 @@ const MappingTestQueryEditor = observer( const embeddedQueryBuilderState = editorStore.embeddedQueryBuilderState; await flowResult( embeddedQueryBuilderState.setEmbeddedQueryBuilderConfiguration({ - setupQueryBuilderState: (): QueryBuilderState => { + setupQueryBuilderState: async (): Promise => { const queryBuilderState = new MappingExecutionQueryBuilderState( embeddedQueryBuilderState.editorStore.applicationStore, embeddedQueryBuilderState.editorStore.graphManagerState, diff --git a/packages/legend-application-studio/src/components/editor/editor-group/service-editor/ServiceExecutionQueryEditor.tsx b/packages/legend-application-studio/src/components/editor/editor-group/service-editor/ServiceExecutionQueryEditor.tsx index a3513a168e..ad129dd0fc 100644 --- a/packages/legend-application-studio/src/components/editor/editor-group/service-editor/ServiceExecutionQueryEditor.tsx +++ b/packages/legend-application-studio/src/components/editor/editor-group/service-editor/ServiceExecutionQueryEditor.tsx @@ -135,7 +135,7 @@ export const ServiceExecutionQueryEditor = observer( executionState.selectedExecutionContextState; await flowResult( embeddedQueryBuilderState.setEmbeddedQueryBuilderConfiguration({ - setupQueryBuilderState: (): QueryBuilderState => { + setupQueryBuilderState: async (): Promise => { const sourceInfo = { service: service.path, ...editorStore.editorMode.getSourceInfo(), @@ -486,7 +486,7 @@ export const queryService = async ( }; await flowResult( embeddedQueryBuilderState.setEmbeddedQueryBuilderConfiguration({ - setupQueryBuilderState: (): QueryBuilderState => { + setupQueryBuilderState: async (): Promise => { const queryBuilderState = new ServiceQueryBuilderState( embeddedQueryBuilderState.editorStore.applicationStore, embeddedQueryBuilderState.editorStore.graphManagerState, diff --git a/packages/legend-application-studio/src/components/editor/editor-group/uml-editor/ClassQueryBuilder.tsx b/packages/legend-application-studio/src/components/editor/editor-group/uml-editor/ClassQueryBuilder.tsx index 6e5d8f9f11..dd01bab989 100644 --- a/packages/legend-application-studio/src/components/editor/editor-group/uml-editor/ClassQueryBuilder.tsx +++ b/packages/legend-application-studio/src/components/editor/editor-group/uml-editor/ClassQueryBuilder.tsx @@ -376,7 +376,7 @@ export const queryClass = async ( const embeddedQueryBuilderState = editorStore.embeddedQueryBuilderState; await flowResult( embeddedQueryBuilderState.setEmbeddedQueryBuilderConfiguration({ - setupQueryBuilderState: () => { + setupQueryBuilderState: async () => { const queryBuilderState = new ClassQueryBuilderState( embeddedQueryBuilderState.editorStore.applicationStore, embeddedQueryBuilderState.editorStore.graphManagerState, diff --git a/packages/legend-application-studio/src/stores/editor/EmbeddedQueryBuilderState.ts b/packages/legend-application-studio/src/stores/editor/EmbeddedQueryBuilderState.ts index 03581d7bd7..7b5f5ee2eb 100644 --- a/packages/legend-application-studio/src/stores/editor/EmbeddedQueryBuilderState.ts +++ b/packages/legend-application-studio/src/stores/editor/EmbeddedQueryBuilderState.ts @@ -30,7 +30,7 @@ type EmbeddedQueryBuilderActionConfiguration = { }; type EmbeddedQueryBuilderConfiguration = { - setupQueryBuilderState: () => QueryBuilderState; + setupQueryBuilderState: () => Promise; disableCompile?: boolean | undefined; actionConfigs: EmbeddedQueryBuilderActionConfiguration[]; }; @@ -100,7 +100,8 @@ export class EmbeddedQueryBuilderState { } } if (!this.editorStore.graphState.error) { - this.queryBuilderState = config.setupQueryBuilderState(); + this.queryBuilderState = + (yield config.setupQueryBuilderState()) as QueryBuilderState; this.actionConfigs = config.actionConfigs; this.editorStore.applicationStore.layoutService.setBackdropContainerElementID( QUERY_BUILDER_COMPONENT_ELEMENT_ID.BACKDROP_CONTAINER, diff --git a/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/database/QueryDatabaseState.ts b/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/database/QueryDatabaseState.ts index 86f5594d7d..e52acad8f3 100644 --- a/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/database/QueryDatabaseState.ts +++ b/packages/legend-application-studio/src/stores/editor/editor-state/element-editor-state/database/QueryDatabaseState.ts @@ -226,7 +226,7 @@ export class QueryDatabaseState { ); yield flowResult( embeddedQueryBuilderState.setEmbeddedQueryBuilderConfiguration({ - setupQueryBuilderState: () => queryBuilderState, + setupQueryBuilderState: async () => queryBuilderState, actionConfigs: [], }), ); diff --git a/packages/legend-application-studio/src/stores/editor/editor-state/end-to-end-workflow-state/QueryConnectionEndToEndWorkflowEditorState.ts b/packages/legend-application-studio/src/stores/editor/editor-state/end-to-end-workflow-state/QueryConnectionEndToEndWorkflowEditorState.ts index 24fc11e1bc..e9371d1f5c 100644 --- a/packages/legend-application-studio/src/stores/editor/editor-state/end-to-end-workflow-state/QueryConnectionEndToEndWorkflowEditorState.ts +++ b/packages/legend-application-studio/src/stores/editor/editor-state/end-to-end-workflow-state/QueryConnectionEndToEndWorkflowEditorState.ts @@ -209,7 +209,7 @@ export class QueryConnectionConfirmationAndGrammarEditorStepperState extends Con yield flowResult( this.editorStore.embeddedQueryBuilderState.setEmbeddedQueryBuilderConfiguration( { - setupQueryBuilderState: () => queryBuilderState, + setupQueryBuilderState: async () => queryBuilderState, actionConfigs: [], }, ), diff --git a/packages/legend-extension-dsl-data-space-studio/src/components/DataSpaceQueryAction.tsx b/packages/legend-extension-dsl-data-space-studio/src/components/DataSpaceQueryAction.tsx index aceb3d325b..523759e2e9 100644 --- a/packages/legend-extension-dsl-data-space-studio/src/components/DataSpaceQueryAction.tsx +++ b/packages/legend-extension-dsl-data-space-studio/src/components/DataSpaceQueryAction.tsx @@ -39,7 +39,7 @@ export const queryDataSpace = async ( const embeddedQueryBuilderState = editorStore.embeddedQueryBuilderState; await flowResult( embeddedQueryBuilderState.setEmbeddedQueryBuilderConfiguration({ - setupQueryBuilderState: () => { + setupQueryBuilderState: async () => { const sourceInfo = Object.assign( {}, editorStore.editorMode.getSourceInfo(), @@ -54,8 +54,9 @@ export const queryDataSpace = async ( QueryBuilderActionConfig.INSTANCE, dataSpace, dataSpace.defaultExecutionContext, + false, undefined, - (dataSpaceInfo: DataSpaceInfo) => { + async (dataSpaceInfo: DataSpaceInfo) => { queryBuilderState.dataSpace = guaranteeType( queryBuilderState.graphManagerState.graph.getElement( dataSpaceInfo.path, @@ -65,9 +66,7 @@ export const queryDataSpace = async ( queryBuilderState.setExecutionContext( queryBuilderState.dataSpace.defaultExecutionContext, ); - queryBuilderState.propagateExecutionContextChange( - queryBuilderState.dataSpace.defaultExecutionContext, - ); + await queryBuilderState.propagateExecutionContextChange(); }, undefined, undefined, @@ -79,9 +78,7 @@ export const queryDataSpace = async ( queryBuilderState.setExecutionContext( dataSpace.defaultExecutionContext, ); - queryBuilderState.propagateExecutionContextChange( - dataSpace.defaultExecutionContext, - ); + await queryBuilderState.propagateExecutionContextChange(); return queryBuilderState; }, actionConfigs: [], diff --git a/packages/legend-extension-dsl-data-space/src/components/DSL_DataSpace_LegendApplicationPlugin.tsx b/packages/legend-extension-dsl-data-space/src/components/DSL_DataSpace_LegendApplicationPlugin.tsx index 79d0d95f5f..dd2dfb56c0 100644 --- a/packages/legend-extension-dsl-data-space/src/components/DSL_DataSpace_LegendApplicationPlugin.tsx +++ b/packages/legend-extension-dsl-data-space/src/components/DSL_DataSpace_LegendApplicationPlugin.tsx @@ -15,11 +15,11 @@ */ import { - LegendApplicationPlugin, type LegendApplicationSetup, type LegendApplicationPluginManager, - collectKeyedCommandConfigEntriesFromConfig, type KeyedCommandConfigEntry, + collectKeyedCommandConfigEntriesFromConfig, + LegendApplicationPlugin, } from '@finos/legend-application'; import packageJson from '../../package.json' with { type: 'json' }; import type { @@ -32,7 +32,7 @@ import type { } from '@finos/legend-query-builder'; import { DataSpaceQueryBuilderState } from '../stores/query-builder/DataSpaceQueryBuilderState.js'; import { DSL_DATA_SPACE_LEGEND_APPLICATION_COMMAND_CONFIG } from '../__lib__/DSL_DataSpace_LegendApplicationCommand.js'; -import type { QuerySearchSpecification } from '@finos/legend-graph'; +import { type QuerySearchSpecification } from '@finos/legend-graph'; import { configureDataGridComponent } from '@finos/legend-lego/data-grid'; import { DataSpaceExecutableTemplate } from '../graph/metamodel/pure/model/packageableElements/dataSpace/DSL_DataSpace_DataSpace.js'; import { filterByType } from '@finos/legend-shared'; @@ -175,10 +175,10 @@ export class DSL_DataSpace_LegendApplicationPlugin } return []; }, - loadCuratedTemplateQuery: ( + loadCuratedTemplateQuery: async ( templateQuery: CuratedTemplateQuery, queryBuilderState: QueryBuilderState, - ): void => { + ): Promise => { if (queryBuilderState instanceof DataSpaceQueryBuilderState) { if ( queryBuilderState.executionContext.name !== @@ -190,9 +190,7 @@ export class DSL_DataSpace_LegendApplicationPlugin ); if (executionContext) { queryBuilderState.setExecutionContext(executionContext); - queryBuilderState.propagateExecutionContextChange( - executionContext, - ); + await queryBuilderState.propagateExecutionContextChange(); queryBuilderState.initializeWithQuery(templateQuery.query); queryBuilderState.onExecutionContextChange?.(executionContext); } diff --git a/packages/legend-extension-dsl-data-space/src/components/query-builder/DataSpaceQueryBuilder.tsx b/packages/legend-extension-dsl-data-space/src/components/query-builder/DataSpaceQueryBuilder.tsx index 0b740482bd..ae971af90e 100644 --- a/packages/legend-extension-dsl-data-space/src/components/query-builder/DataSpaceQueryBuilder.tsx +++ b/packages/legend-extension-dsl-data-space/src/components/query-builder/DataSpaceQueryBuilder.tsx @@ -45,6 +45,7 @@ import { } from '@finos/legend-query-builder'; import { type Runtime, + type PackageableRuntime, getMappingCompatibleRuntimes, PackageableElementExplicitReference, RuntimePointer, @@ -89,6 +90,25 @@ export const formatDataSpaceOptionLabel = (
); +const resolveExecutionContextRuntimes = ( + queryBuilderState: DataSpaceQueryBuilderState, +): PackageableRuntime[] => { + if (queryBuilderState.dataSpaceAnalysisResult) { + const executionContext = Array.from( + queryBuilderState.dataSpaceAnalysisResult.executionContextsIndex.values(), + ).find( + (e) => + e.mapping.path === + queryBuilderState.executionContext.mapping.value.path, + ); + return guaranteeNonNullable(executionContext).compatibleRuntimes; + } + return getMappingCompatibleRuntimes( + queryBuilderState.executionContext.mapping.value, + queryBuilderState.graphManagerState.usableRuntimes, + ); +}; + export type ExecutionContextOption = { label: string; value: DataSpaceExecutionContext; @@ -152,7 +172,9 @@ const DataSpaceQueryBuilderSetupPanelContent = observer( }, }; const onDataSpaceOptionChange = (option: DataSpaceOption): void => { - queryBuilderState.onDataSpaceChange(option.value); + queryBuilderState + .onDataSpaceChange(option.value) + .catch(queryBuilderState.applicationStore.alertUnhandledError); }; const openDataSpaceAdvancedSearch = (): void => { @@ -170,22 +192,30 @@ const DataSpaceQueryBuilderSetupPanelContent = observer( const selectedExecutionContextOption = buildExecutionContextOption( queryBuilderState.executionContext, ); - const onExecutionContextOptionChange = ( + + const onExecutionContextOptionChange = async ( option: ExecutionContextOption, - ): void => { + ): Promise => { if (option.value === queryBuilderState.executionContext) { return; } + const currentMapping = + queryBuilderState.executionContext.mapping.value.path; queryBuilderState.setExecutionContext(option.value); - queryBuilderState.propagateExecutionContextChange(option.value); + await queryBuilderState.propagateExecutionContextChange( + currentMapping === option.value.mapping.value.path, + ); queryBuilderState.onExecutionContextChange?.(option.value); }; + const handleExecutionContextOptionChange = ( + option: ExecutionContextOption, + ): void => { + flowResult(onExecutionContextOptionChange(option)); + }; + // runtime - const runtimeOptions = getMappingCompatibleRuntimes( - queryBuilderState.executionContext.mapping.value, - queryBuilderState.graphManagerState.usableRuntimes, - ) + const runtimeOptions = resolveExecutionContextRuntimes(queryBuilderState) .map( (rt) => new RuntimePointer(PackageableElementExplicitReference.create(rt)), @@ -220,6 +250,7 @@ const DataSpaceQueryBuilderSetupPanelContent = observer( queryBuilderState.dataSpace, queryBuilderState.executionContext.mapping.value, queryBuilderState.graphManagerState, + queryBuilderState, ); useEffect(() => { @@ -373,7 +404,7 @@ const DataSpaceQueryBuilderSetupPanelContent = observer( (executionContextOptions.length === 1 && Boolean(selectedExecutionContextOption)) } - onChange={onExecutionContextOptionChange} + onChange={handleExecutionContextOptionChange} value={selectedExecutionContextOption} darkMode={ !applicationStore.layoutService diff --git a/packages/legend-extension-dsl-data-space/src/components/query-builder/DataSpaceQueryBuilderTemplateQueryPanelContent.tsx b/packages/legend-extension-dsl-data-space/src/components/query-builder/DataSpaceQueryBuilderTemplateQueryPanelContent.tsx index 1a5e8f0827..7a997b938a 100644 --- a/packages/legend-extension-dsl-data-space/src/components/query-builder/DataSpaceQueryBuilderTemplateQueryPanelContent.tsx +++ b/packages/legend-extension-dsl-data-space/src/components/query-builder/DataSpaceQueryBuilderTemplateQueryPanelContent.tsx @@ -70,7 +70,7 @@ const DataSpaceTemplateQueryDialog = observer( executionContext.name !== queryBuilderState.executionContext.name ) { queryBuilderState.setExecutionContext(executionContext); - queryBuilderState.propagateExecutionContextChange(executionContext); + await queryBuilderState.propagateExecutionContextChange(); queryBuilderState.initializeWithQuery(query); queryBuilderState.onExecutionContextChange?.(executionContext); } else { diff --git a/packages/legend-extension-dsl-data-space/src/graph-manager/index.ts b/packages/legend-extension-dsl-data-space/src/graph-manager/index.ts index 5e3d60224f..547569b692 100644 --- a/packages/legend-extension-dsl-data-space/src/graph-manager/index.ts +++ b/packages/legend-extension-dsl-data-space/src/graph-manager/index.ts @@ -39,3 +39,7 @@ export * from '../stores/shared/DataSpaceInfo.js'; export * from '../stores/query-builder/DataSpaceQueryBuilderState.js'; export { observe_DataSpace } from '../graph-manager/action/changeDetection/DSL_DataSpace_ObserverHelper.js'; +export { + V1_DataSpaceExecutionContext, + V1_DataSpace, +} from '../graph-manager/protocol/pure/v1/model/packageableElements/dataSpace/V1_DSL_DataSpace_DataSpace.js'; diff --git a/packages/legend-extension-dsl-data-space/src/graph-manager/protocol/pure/DSL_DataSpace_PureGraphManagerExtension.ts b/packages/legend-extension-dsl-data-space/src/graph-manager/protocol/pure/DSL_DataSpace_PureGraphManagerExtension.ts index d284c457f4..7fde9e312a 100644 --- a/packages/legend-extension-dsl-data-space/src/graph-manager/protocol/pure/DSL_DataSpace_PureGraphManagerExtension.ts +++ b/packages/legend-extension-dsl-data-space/src/graph-manager/protocol/pure/DSL_DataSpace_PureGraphManagerExtension.ts @@ -19,8 +19,10 @@ import { type PureProtocolProcessorPlugin, AbstractPureGraphManagerExtension, type QueryInfo, + type PureModel, + type GraphManagerOperationReport, } from '@finos/legend-graph'; -import type { Entity } from '@finos/legend-storage'; +import type { Entity, ProjectGAVCoordinates } from '@finos/legend-storage'; import { guaranteeNonNullable, type ActionState, @@ -42,6 +44,22 @@ export abstract class DSL_DataSpace_PureGraphManagerExtension extends AbstractPu actionState?: ActionState, ): Promise; + abstract analyzeDataSpaceCoverage( + dataSpacePath: string, + entitiesRetriever: () => Promise, + entitiesWithClassifierRetriever: () => Promise< + [PlainObject[], PlainObject[]] + >, + cacheRetriever?: () => Promise>, + actionState?: ActionState, + graphReport?: GraphManagerOperationReport | undefined, + pureGraph?: PureModel | undefined, + executionContext?: string | undefined, + mappingPath?: string | undefined, + projectInfo?: ProjectGAVCoordinates, + templateQueryId?: string, + ): Promise; + abstract addNewExecutableToDataSpaceEntity( dataSpaceEntity: Entity, currentQuery: QueryInfo, diff --git a/packages/legend-extension-dsl-data-space/src/graph-manager/protocol/pure/v1/V1_DSL_DataSpace_PureGraphManagerExtension.ts b/packages/legend-extension-dsl-data-space/src/graph-manager/protocol/pure/v1/V1_DSL_DataSpace_PureGraphManagerExtension.ts index 11a9dd8b6a..b9fa4a4a63 100644 --- a/packages/legend-extension-dsl-data-space/src/graph-manager/protocol/pure/v1/V1_DSL_DataSpace_PureGraphManagerExtension.ts +++ b/packages/legend-extension-dsl-data-space/src/graph-manager/protocol/pure/v1/V1_DSL_DataSpace_PureGraphManagerExtension.ts @@ -19,6 +19,8 @@ import { type AbstractPureGraphManager, type PureProtocolProcessorPlugin, type MappingModelCoverageAnalysisResult, + type GraphManagerOperationReport, + type FunctionAnalysisInfo, PureModel, V1_PureGraphManager, PureClientVersion, @@ -36,8 +38,13 @@ import { V1_deserializePackageableElement, QueryDataSpaceExecutionContextInfo, V1_RemoteEngine, + LegendSDLC, + PackageableElementPointerType, + V1_PackageableElementPointer, + V1_ConcreteFunctionDefinition, + V1_buildFunctionInfoAnalysis, } from '@finos/legend-graph'; -import type { Entity } from '@finos/legend-storage'; +import type { Entity, ProjectGAVCoordinates } from '@finos/legend-storage'; import { type PlainObject, ActionState, @@ -56,6 +63,7 @@ import { } from '../../../../graph/metamodel/pure/model/packageableElements/dataSpace/DSL_DataSpace_DataSpace.js'; import { V1_DataSpace, + V1_DataSpaceExecutionContext, V1_DataSpaceSupportCombinedInfo, V1_DataSpaceSupportEmail, V1_DataSpaceTemplateExecutable, @@ -96,6 +104,7 @@ import { V1_DataSpaceFunctionPointerExecutableInfo, } from './engine/analytics/V1_DataSpaceAnalysis.js'; import { getDiagram } from '@finos/legend-extension-dsl-diagram/graph'; +import { resolveVersion } from '@finos/legend-server-depot'; const ANALYZE_DATA_SPACE_TRACE = 'analyze data space'; const TEMPORARY__TDS_SAMPLE_VALUES__DELIMETER = '-- e.g.'; @@ -219,6 +228,112 @@ export class V1_DSL_DataSpace_PureGraphManagerExtension extends DSL_DataSpace_Pu ); } + retrieveExecutionContextFromTemplateQueryId( + dataSpaceAnalysisResult: PlainObject, + templateQueryId: string, + plugins: PureProtocolProcessorPlugin[], + ): string | undefined { + const analysisResult = V1_deserializeDataSpaceAnalysisResult( + dataSpaceAnalysisResult, + plugins, + ); + let execContext = undefined; + const info = analysisResult.executables.find( + (ex) => ex.info?.id === templateQueryId, + )?.info; + if (info) { + execContext = + info.executionContextKey ?? analysisResult.defaultExecutionContext; + } + return execContext; + } + + async analyzeDataSpaceCoverage( + dataSpacePath: string, + entitiesRetriever: () => Promise, + entitiesWithClassifierRetriever: () => Promise< + [PlainObject[], PlainObject[]] + >, + cacheRetriever?: () => Promise>, + actionState?: ActionState, + graphReport?: GraphManagerOperationReport | undefined, + pureGraph?: PureModel | undefined, + executionContext?: string | undefined, + mappingPath?: string | undefined, + projectInfo?: ProjectGAVCoordinates, + templateQueryId?: string, + ): Promise { + const cacheResult = cacheRetriever + ? await this.fetchDataSpaceAnalysisFromCache(cacheRetriever, actionState) + : undefined; + const engineClient = guaranteeType( + this.graphManager.engine, + V1_RemoteEngine, + 'analyzeDataSpaceCoverage is only supported by remote engine', + ).getEngineServerClient(); + let analysisResult: PlainObject; + let cachedAnalysisResult; + if (cacheResult) { + cachedAnalysisResult = V1_deserializeDataSpaceAnalysisResult( + cacheResult, + this.graphManager.pluginManager.getPureProtocolProcessorPlugins(), + ); + } + if ( + cacheResult && + cachedAnalysisResult?.executionContexts.every( + (e) => + e.mappingModelCoverageAnalysisResult?.model !== undefined || + cachedAnalysisResult.mappingToMappingCoverageResult?.get(e.mapping) + ?.model !== undefined, + ) + ) { + analysisResult = cacheResult; + } else { + actionState?.setMessage('Fetching project entities and dependencies...'); + const entities = await entitiesRetriever(); + actionState?.setMessage('Analyzing data space...'); + analysisResult = await engineClient.postWithTracing< + PlainObject + >( + engineClient.getTraceData(ANALYZE_DATA_SPACE_TRACE), + `${engineClient._pure()}/analytics/dataSpace/coverage`, + { + clientVersion: V1_PureGraphManager.DEV_PROTOCOL_VERSION, + dataSpace: dataSpacePath, + model: { + _type: V1_PureModelContextType.DATA, + elements: entities.map((entity) => entity.content), + }, + }, + {}, + undefined, + undefined, + { enableCompression: true }, + ); + } + const plugins = + this.graphManager.pluginManager.getPureProtocolProcessorPlugins(); + return this.buildDataSpaceAnalytics( + analysisResult, + plugins, + graphReport, + pureGraph, + executionContext + ? executionContext + : templateQueryId + ? this.retrieveExecutionContextFromTemplateQueryId( + analysisResult, + templateQueryId, + plugins, + ) + : undefined, + mappingPath, + projectInfo, + entitiesWithClassifierRetriever, + ); + } + async retrieveDataSpaceAnalysisFromCache( cacheRetriever: () => Promise>, actionState?: ActionState, @@ -255,11 +370,78 @@ export class V1_DSL_DataSpace_PureGraphManagerExtension extends DSL_DataSpace_Pu return cacheResult; } + // build function analysis info by fetching functions within this project from metadata when building minimal graph + async processFunctionForMinimalGraph( + entitiesWithClassifierRetriever: () => Promise< + [PlainObject[], PlainObject[]] + >, + graph: PureModel, + dataSpaceAnalysisResult: DataSpaceAnalysisResult, + plugins: PureProtocolProcessorPlugin[], + ): Promise { + const [functionEntities, dependencyFunctionEntities]: [ + PlainObject[], + PlainObject[], + ] = await entitiesWithClassifierRetriever(); + const functionProtocols = functionEntities.map((func) => + guaranteeType( + V1_deserializePackageableElement( + (func.entity as Entity).content, + plugins, + ), + V1_ConcreteFunctionDefinition, + ), + ); + const dependencyFunctionProtocols = dependencyFunctionEntities.map((func) => + guaranteeType( + V1_deserializePackageableElement( + (func.entity as Entity).content, + plugins, + ), + V1_ConcreteFunctionDefinition, + ), + ); + const functionInfos = V1_buildFunctionInfoAnalysis( + functionProtocols, + graph, + ); + const dependencyFunctionInfos = V1_buildFunctionInfoAnalysis( + dependencyFunctionProtocols, + graph, + ); + if (functionInfos.length > 0) { + const functionInfoMap = new Map(); + functionInfos.forEach((funcInfo) => { + functionInfoMap.set(funcInfo.functionPath, funcInfo); + }); + dataSpaceAnalysisResult.functionInfos = functionInfoMap; + } + if (dependencyFunctionInfos.length > 0) { + const dependencyFunctionInfoMap = new Map(); + functionInfos.forEach((funcInfo) => { + dependencyFunctionInfoMap.set(funcInfo.functionPath, funcInfo); + }); + dataSpaceAnalysisResult.dependencyFunctionInfos = + dependencyFunctionInfoMap; + } + } + async buildDataSpaceAnalytics( - json: PlainObject, + analytics: PlainObject, plugins: PureProtocolProcessorPlugin[], + graphReport?: GraphManagerOperationReport | undefined, + pureGraph?: PureModel | undefined, + executionContext?: string | undefined, + mappingPath?: string | undefined, + projectInfo?: ProjectGAVCoordinates, + entitiesWithClassifierRetriever?: () => Promise< + [PlainObject[], PlainObject[]] + >, ): Promise { - const analysisResult = V1_deserializeDataSpaceAnalysisResult(json, plugins); + const analysisResult = V1_deserializeDataSpaceAnalysisResult( + analytics, + plugins, + ); const result = new DataSpaceAnalysisResult(); result.name = analysisResult.name; result.package = analysisResult.package; @@ -306,26 +488,30 @@ export class V1_DSL_DataSpace_PureGraphManagerExtension extends DSL_DataSpace_Pu // NOTE: we will relax the check and not throw here for unknown support info type } - // create an empty graph - const systemModel = new SystemModel( - this.graphManager.pluginManager.getPureGraphPlugins(), - ); - const coreModel = new CoreModel( - this.graphManager.pluginManager.getPureGraphPlugins(), - ); - await this.graphManager.buildSystem( - coreModel, - systemModel, - ActionState.create(), - {}, - ); - systemModel.initializeAutoImports(); - const graph = new PureModel( - coreModel, - systemModel, - this.graphManager.pluginManager.getPureGraphPlugins(), - ); - + let graphEntities; + let graph: PureModel; + let minialGraph = false; + if (pureGraph) { + graph = pureGraph; + } else { + // create an empty graph + const extensionElementClasses = + this.graphManager.pluginManager.getPureGraphPlugins(); + const systemModel = new SystemModel(extensionElementClasses); + const coreModel = new CoreModel(extensionElementClasses); + await this.graphManager.buildSystem( + coreModel, + systemModel, + ActionState.create(), + {}, + ); + systemModel.initializeAutoImports(); + graph = new PureModel( + coreModel, + systemModel, + this.graphManager.pluginManager.getPureGraphPlugins(), + ); + } // Create dummy mappings and runtimes // TODO?: these stubbed mappings and runtimes are not really useful that useful, so either we should // simplify the model here or potentially refactor the backend analytics endpoint to return these as model @@ -355,55 +541,142 @@ export class V1_DSL_DataSpace_PureGraphManagerExtension extends DSL_DataSpace_Pu return runtime; }); - // prepare the model context data - const graphEntities = analysisResult.model.elements - // NOTE: this is a temporary hack to fix a problem with data space analytics - // where the classes for properties are not properly surveyed - // We need to wait for the actual fix in backend to be merged and released - // See https://github.com/finos/legend-engine/pull/836 - .concat( - uniq( - analysisResult.model.elements.flatMap((element) => { - if (element instanceof V1_Class) { - return element.derivedProperties - .map((prop) => prop.returnType) - .concat(element.properties.map((prop) => prop.type)); - } - return []; - }), - ) - // make sure to not include types already returned by the analysis - .filter( - (path) => - !analysisResult.model.elements - .map((el) => el.path) - .includes(path), - ) - .map((path) => { - const [pkgPath, name] = resolvePackagePathAndElementName(path); - if (!pkgPath) { - // exclude package-less elements (i.e. primitive types) - return undefined; - } - const _class = new V1_Class(); - _class.name = name; - _class.package = pkgPath; - return _class; - }) - .filter(isNonNullable), - ) - .concat(mappingModels) - .concat(runtimeModels) - // NOTE: if an element could be found in the graph already it means it comes from system - // so we could rid of it - .filter((el) => !graph.getNullableElement(el.path, false)) - .map((el) => this.graphManager.elementProtocolToEntity(el)); - - await this.graphManager.buildGraph( - graph, - graphEntities, - ActionState.create(), + // The DataSpace entity is excluded from AnalyticsResult.Json to reduce the JSON size + // because all its information can be found in V1_DataSpaceAnalysisResult. + // Therefore, we are building a simple v1_DataSpace entity based on V1_DataSpaceAnalysisResult. + const dataspaceEntity = new V1_DataSpace(); + dataspaceEntity.name = analysisResult.name; + dataspaceEntity.package = analysisResult.package; + dataspaceEntity.supportInfo = analysisResult.supportInfo; + dataspaceEntity.executionContexts = analysisResult.executionContexts.map( + (execContext) => { + const contextProtocol = new V1_DataSpaceExecutionContext(); + contextProtocol.name = execContext.name; + contextProtocol.title = execContext.title; + contextProtocol.description = execContext.description; + contextProtocol.mapping = new V1_PackageableElementPointer( + PackageableElementPointerType.MAPPING, + execContext.mapping, + ); + contextProtocol.defaultRuntime = new V1_PackageableElementPointer( + PackageableElementPointerType.RUNTIME, + execContext.defaultRuntime, + ); + return contextProtocol; + }, ); + dataspaceEntity.defaultExecutionContext = + analysisResult.defaultExecutionContext; + dataspaceEntity.title = analysisResult.title; + dataspaceEntity.description = analysisResult.description; + + const resolvedMappingPath = + mappingPath ?? + analysisResult.executionContexts.find( + (value) => + value.name === + (executionContext ?? analysisResult.defaultExecutionContext), + )?.mapping; + let pmcd; + if (resolvedMappingPath) { + const mappingModelCoverageAnalysisResult = + analysisResult.mappingToMappingCoverageResult?.get(resolvedMappingPath); + pmcd = mappingModelCoverageAnalysisResult?.model; + } + if (pmcd && projectInfo) { + graphEntities = pmcd.elements + .concat(mappingModels) + .concat(runtimeModels) + .concat(dataspaceEntity) + // NOTE: if an element could be found in the graph already it means it comes from system + // so we could rid of it + .filter((el) => !graph.getNullableElement(el.path, false)) + .map((el) => this.graphManager.elementProtocolToEntity(el)); + await this.graphManager.buildGraph( + graph, + graphEntities, + ActionState.create(), + { + origin: new LegendSDLC( + projectInfo.groupId, + projectInfo.artifactId, + resolveVersion(projectInfo.versionId), + ), + }, + graphReport, + ); + if (entitiesWithClassifierRetriever) { + try { + await this.processFunctionForMinimalGraph( + entitiesWithClassifierRetriever, + graph, + result, + plugins, + ); + } catch { + //do nothing + } + } + minialGraph = true; + } else { + const elements = analysisResult.model.elements + // NOTE: this is a temporary hack to fix a problem with data space analytics + // where the classes for properties are not properly surveyed + // We need to wait for the actual fix in backend to be merged and released + // See https://github.com/finos/legend-engine/pull/836 + .concat( + uniq( + analysisResult.model.elements.flatMap((element) => { + if (element instanceof V1_Class) { + return element.derivedProperties + .map((prop) => prop.returnType) + .concat(element.properties.map((prop) => prop.type)); + } + return []; + }), + ) + // make sure to not include types already returned by the analysis + .filter( + (path) => + !analysisResult.model.elements + .map((el) => el.path) + .includes(path), + ) + .map((path) => { + const [pkgPath, name] = resolvePackagePathAndElementName(path); + if (!pkgPath) { + // exclude package-less elements (i.e. primitive types) + return undefined; + } + const _class = new V1_Class(); + _class.name = name; + _class.package = pkgPath; + return _class; + }) + .filter(isNonNullable), + ) + .concat(mappingModels) + .concat(runtimeModels); + const alreadyContainsDataspace = elements.find( + (e) => e.path === dataspaceEntity.path, + ); + let allElements = elements; + if (!alreadyContainsDataspace) { + allElements = elements.concat(dataspaceEntity); + } + // prepare the model context data + graphEntities = allElements + // NOTE: if an element could be found in the graph already it means it comes from system + // so we could rid of it + .filter((el) => !graph.getNullableElement(el.path, false)) + .map((el) => this.graphManager.elementProtocolToEntity(el)); + + await this.graphManager.buildGraph( + graph, + graphEntities, + ActionState.create(), + ); + } const mappingToMappingCoverageResult = new Map< string, @@ -414,7 +687,11 @@ export class V1_DSL_DataSpace_PureGraphManagerExtension extends DSL_DataSpace_Pu ([key, value]) => { mappingToMappingCoverageResult.set( key, - V1_buildModelCoverageAnalysisResult(value, graph.getMapping(key)), + V1_buildModelCoverageAnalysisResult( + value, + this.graphManager, + graph.getMapping(key), + ), ); }, ); @@ -455,6 +732,7 @@ export class V1_DSL_DataSpace_PureGraphManagerExtension extends DSL_DataSpace_Pu context.mapping, V1_buildModelCoverageAnalysisResult( context.mappingModelCoverageAnalysisResult, + this.graphManager, contextAnalysisResult.mapping, ), ); @@ -586,13 +864,15 @@ export class V1_DSL_DataSpace_PureGraphManagerExtension extends DSL_DataSpace_Pu }); // diagrams - result.diagrams = analysisResult.diagrams.map((diagramProtocol) => { - const diagram = new DataSpaceDiagramAnalysisResult(); - diagram.title = diagramProtocol.title; - diagram.description = diagramProtocol.description; - diagram.diagram = getDiagram(diagramProtocol.diagram, graph); - return diagram; - }); + result.diagrams = (minialGraph ? [] : analysisResult.diagrams).map( + (diagramProtocol) => { + const diagram = new DataSpaceDiagramAnalysisResult(); + diagram.title = diagramProtocol.title; + diagram.description = diagramProtocol.description; + diagram.diagram = getDiagram(diagramProtocol.diagram, graph); + return diagram; + }, + ); // executables result.executables = analysisResult.executables.map( diff --git a/packages/legend-extension-dsl-data-space/src/stores/query-builder/DataSpaceQueryBuilderState.ts b/packages/legend-extension-dsl-data-space/src/stores/query-builder/DataSpaceQueryBuilderState.ts index ec3aaaf354..49e1a08f67 100644 --- a/packages/legend-extension-dsl-data-space/src/stores/query-builder/DataSpaceQueryBuilderState.ts +++ b/packages/legend-extension-dsl-data-space/src/stores/query-builder/DataSpaceQueryBuilderState.ts @@ -34,7 +34,6 @@ import { Package, QueryDataSpaceExecutionContext, elementBelongsToPackage, - RuntimePointer, } from '@finos/legend-graph'; import { type DepotServerClient, @@ -47,7 +46,6 @@ import { ActionState, assertErrorThrown, filterByType, - getNullableFirstEntry, } from '@finos/legend-shared'; import { action, flow, makeObservable, observable } from 'mobx'; import { renderDataSpaceQueryBuilderSetupPanelContent } from '../../components/query-builder/DataSpaceQueryBuilder.js'; @@ -88,11 +86,34 @@ export const resolveUsableDataSpaceClasses = ( dataSpace: DataSpace, mapping: Mapping, graphManagerState: GraphManagerState, + queryBuilderState?: DataSpaceQueryBuilderState, ): Class[] => { - const compatibleClasses = getMappingCompatibleClasses( + let compatibleClasses = getMappingCompatibleClasses( mapping, graphManagerState.usableClasses, ); + const mappingModelCoverageAnalysisResult = + queryBuilderState?.dataSpaceAnalysisResult?.mappingToMappingCoverageResult?.get( + mapping.path, + ); + if ( + // This check is to make sure that we have `info` field present in `MappedEntity` which + // contains information about the mapped class path + mappingModelCoverageAnalysisResult?.mappedEntities.some( + (m) => m.info !== undefined, + ) + ) { + const compatibleClassPaths = + mappingModelCoverageAnalysisResult.mappedEntities.map( + (e) => e.info?.classPath, + ); + const uniqueCompatibleClasses = compatibleClassPaths.filter( + (val, index) => compatibleClassPaths.indexOf(val) === index, + ); + compatibleClasses = graphManagerState.graph.classes.filter((c) => + uniqueCompatibleClasses.includes(c.path), + ); + } if (dataSpace.elements?.length) { const elements = dataSpace.elements; return compatibleClasses.filter((_class) => { @@ -335,7 +356,7 @@ export class DataSpacesDepotRepository extends DataSpacesBuilderRepoistory { } export class DataSpaceQueryBuilderState extends QueryBuilderState { - readonly onDataSpaceChange: (val: DataSpaceInfo) => void; + readonly onDataSpaceChange: (val: DataSpaceInfo) => Promise; readonly onExecutionContextChange?: | ((val: DataSpaceExecutionContext) => void) | undefined; @@ -351,6 +372,7 @@ export class DataSpaceQueryBuilderState extends QueryBuilderState { executionContext!: DataSpaceExecutionContext; showRuntimeSelector = false; isTemplateQueryDialogOpen = false; + isLightGraphEnabled!: boolean; displayedTemplateQueries: DataSpaceExecutableAnalysisResult[] | undefined; constructor( @@ -360,8 +382,9 @@ export class DataSpaceQueryBuilderState extends QueryBuilderState { actionConfig: QueryBuilderActionConfig, dataSpace: DataSpace, executionContext: DataSpaceExecutionContext, + isLightGraphEnabled: boolean, dataSpaceRepo: DataSpacesBuilderRepoistory | undefined, - onDataSpaceChange: (val: DataSpaceInfo) => void, + onDataSpaceChange: (val: DataSpaceInfo) => Promise, dataSpaceAnalysisResult?: DataSpaceAnalysisResult | undefined, onExecutionContextChange?: | ((val: DataSpaceExecutionContext) => void) @@ -377,10 +400,12 @@ export class DataSpaceQueryBuilderState extends QueryBuilderState { executionContext: observable, showRuntimeSelector: observable, isTemplateQueryDialogOpen: observable, + isLightGraphEnabled: observable, displayedTemplateQueries: observable, setExecutionContext: action, setShowRuntimeSelector: action, setTemplateQueryDialogOpen: action, + setIsLightGraphEnabled: action, intialize: flow, }); @@ -425,41 +450,8 @@ export class DataSpaceQueryBuilderState extends QueryBuilderState { this.showRuntimeSelector = val; } - /** - * Propagation after changing the execution context: - * - The mapping will be updated to the mapping of the execution context - * - The runtime will be updated to the default runtime of the execution context - * - If no class is chosen, try to choose a compatible class - * - If the chosen class is compatible with the new selected execution context mapping, do nothing, otherwise, try to choose a compatible class - */ - propagateExecutionContextChange( - executionContext: DataSpaceExecutionContext, - ): void { - const mapping = executionContext.mapping.value; - this.changeMapping(mapping); - const mappingModelCoverageAnalysisResult = - this.dataSpaceAnalysisResult?.mappingToMappingCoverageResult?.get( - mapping.path, - ); - if (mappingModelCoverageAnalysisResult) { - this.explorerState.mappingModelCoverageAnalysisResult = - mappingModelCoverageAnalysisResult; - } - this.changeRuntime(new RuntimePointer(executionContext.defaultRuntime)); - - const compatibleClasses = resolveUsableDataSpaceClasses( - this.dataSpace, - mapping, - this.graphManagerState, - ); - // if there is no chosen class or the chosen one is not compatible - // with the mapping then pick a compatible class if possible - if (!this.class || !compatibleClasses.includes(this.class)) { - const possibleNewClass = getNullableFirstEntry(compatibleClasses); - if (possibleNewClass) { - this.changeClass(possibleNewClass); - } - } + setIsLightGraphEnabled(val: boolean): void { + this.isLightGraphEnabled = val; } override buildFunctionAnalysisInfo(): diff --git a/packages/legend-graph/src/graph-manager/GraphManagerStatistics.ts b/packages/legend-graph/src/graph-manager/GraphManagerStatistics.ts index 2c26598eeb..86b81d6967 100644 --- a/packages/legend-graph/src/graph-manager/GraphManagerStatistics.ts +++ b/packages/legend-graph/src/graph-manager/GraphManagerStatistics.ts @@ -33,6 +33,7 @@ export type GraphInitializationReport = { graph: GraphManagerOperationReport; generations?: GraphManagerOperationReport; generationCount?: number; + isLightGraphEnabled?: boolean; }; export const createGraphManagerOperationReport = diff --git a/packages/legend-graph/src/graph-manager/action/analytics/MappingModelCoverageAnalysis.ts b/packages/legend-graph/src/graph-manager/action/analytics/MappingModelCoverageAnalysis.ts index 38808ac627..8d8ed66a75 100644 --- a/packages/legend-graph/src/graph-manager/action/analytics/MappingModelCoverageAnalysis.ts +++ b/packages/legend-graph/src/graph-manager/action/analytics/MappingModelCoverageAnalysis.ts @@ -14,19 +14,40 @@ * limitations under the License. */ +import type { Entity } from '@finos/legend-storage'; import type { Mapping } from '../../../graph/metamodel/pure/packageableElements/mapping/Mapping.js'; export type RawMappingModelCoverageAnalysisResult = object; +export class MappedEntityInfo { + readonly __PROPERTIES_INDEX = new Map(); + + classPath: string; + isRootEntity: boolean; + subClasses: string[]; + + constructor(classPath: string, isRootEntity: boolean, subClasses: string[]) { + this.isRootEntity = isRootEntity; + this.subClasses = subClasses; + this.classPath = classPath; + } +} + export class MappedEntity { readonly __PROPERTIES_INDEX = new Map(); path: string; properties: MappedProperty[]; + info?: MappedEntityInfo | undefined; - constructor(path: string, properties: MappedProperty[]) { + constructor( + path: string, + properties: MappedProperty[], + info?: MappedEntityInfo | undefined, + ) { this.path = path; this.properties = properties; + this.info = info; properties.forEach((property) => this.__PROPERTIES_INDEX.set(property.name, property), ); @@ -73,12 +94,18 @@ export class MappingModelCoverageAnalysisResult { readonly mapping: Mapping; mappedEntities: MappedEntity[]; + entities?: Entity[] | undefined; - constructor(mappedEntities: MappedEntity[], mapping: Mapping) { + constructor( + mappedEntities: MappedEntity[], + mapping: Mapping, + entities?: Entity[] | undefined, + ) { this.mappedEntities = mappedEntities; this.mapping = mapping; mappedEntities.forEach((entity) => this.__ENTITIES_INDEX.set(entity.path, entity), ); + this.entities = entities; } } diff --git a/packages/legend-graph/src/graph-manager/action/query/Query.ts b/packages/legend-graph/src/graph-manager/action/query/Query.ts index c13efd31b3..f76ff7ae2f 100644 --- a/packages/legend-graph/src/graph-manager/action/query/Query.ts +++ b/packages/legend-graph/src/graph-manager/action/query/Query.ts @@ -156,11 +156,13 @@ export interface QueryInfo { name: string; id: string; versionId: string; - origignalVersionId?: string | undefined; + originalVersionId?: string | undefined; groupId: string; artifactId: string; executionContext: QueryExecutionContextInfo; mapping?: string | undefined; runtime?: string | undefined; content: string; + isCurrentUserQuery: boolean; + taggedValues?: QueryTaggedValue[] | undefined; } diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/V1_PureGraphManager.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/V1_PureGraphManager.ts index 9391813e3d..cd951d8145 100644 --- a/packages/legend-graph/src/graph-manager/protocol/pure/v1/V1_PureGraphManager.ts +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/V1_PureGraphManager.ts @@ -40,6 +40,7 @@ import { assertNonEmptyString, uniq, guaranteeType, + guaranteeNonEmptyString, } from '@finos/legend-shared'; import type { TEMPORARY__AbstractEngineConfig } from '../../../../graph-manager/action/TEMPORARY__AbstractEngineConfig.js'; import { @@ -192,6 +193,7 @@ import { type Query, QueryExplicitExecutionContextInfo, type QueryInfo, + QueryTaggedValue, } from '../../../../graph-manager/action/query/Query.js'; import { V1_buildQuery, @@ -3042,50 +3044,39 @@ export class V1_PureGraphManager extends AbstractPureGraphManager { ) ).map((protocol) => { // TODO: improve abstraction so that we can get the current user ID from any abstract engine - const currentUserId = - this.engine instanceof V1_RemoteEngine - ? this.engine.getCurrentUserId() - : undefined; - return V1_buildLightQuery(protocol, currentUserId); + return V1_buildLightQuery(protocol, this.engine.getCurrentUserId()); }); } async getQueries(queryIds: string[]): Promise { return (await this.engine.getQueries(queryIds)).map((protocol) => { // TODO: improve abstraction so that we can get the current user ID from any abstract engine - const currentUserId = - this.engine instanceof V1_RemoteEngine - ? this.engine.getCurrentUserId() - : undefined; - return V1_buildLightQuery(protocol, currentUserId); + return V1_buildLightQuery(protocol, this.engine.getCurrentUserId()); }); } async getLightQuery(queryId: string): Promise { // TODO: improve abstraction so that we can get the current user ID from any abstract engine - const currentUserId = - this.engine instanceof V1_RemoteEngine - ? this.engine.getCurrentUserId() - : undefined; return V1_buildLightQuery( await this.engine.getQuery(queryId), - currentUserId, + this.engine.getCurrentUserId(), ); } async getQuery(queryId: string, graph: PureModel): Promise { // TODO: improve abstraction so that we can get the current user ID from any abstract engine - const currentUserId = - this.engine instanceof V1_RemoteEngine - ? this.engine.getCurrentUserId() - : undefined; return V1_buildQuery( await this.engine.getQuery(queryId), graph, - currentUserId, + this.engine.getCurrentUserId(), ); } async getQueryInfo(queryId: string): Promise { + const currentUserId = + this.engine instanceof V1_RemoteEngine + ? this.engine.getCurrentUserId() + : undefined; + const query = await this.engine.getQuery(queryId); return { name: query.name, @@ -3097,45 +3088,51 @@ export class V1_PureGraphManager extends AbstractPureGraphManager { runtime: query.runtime, executionContext: V1_buildExecutionContextInfo(query), content: query.content, + isCurrentUserQuery: + currentUserId !== undefined && query.owner === currentUserId, + taggedValues: query.taggedValues?.map((taggedValueProtocol) => { + const taggedValue = new QueryTaggedValue(); + taggedValue.profile = guaranteeNonEmptyString( + taggedValueProtocol.tag.profile, + `Tagged value 'tag.profile' field is missing or empty`, + ); + taggedValue.tag = guaranteeNonEmptyString( + taggedValueProtocol.tag.value, + `Tagged value 'tag.value' field is missing or empty`, + ); + taggedValue.value = guaranteeNonEmptyString( + taggedValueProtocol.value, + `Tagged value 'value' field is missing or empty`, + ); + return taggedValue; + }), }; } async createQuery(query: Query, graph: PureModel): Promise { // TODO: improve abstraction so that we can get the current user ID from any abstract engine - const currentUserId = - this.engine instanceof V1_RemoteEngine - ? this.engine.getCurrentUserId() - : undefined; return V1_buildQuery( await this.engine.createQuery(V1_transformQuery(query)), graph, - currentUserId, + this.engine.getCurrentUserId(), ); } async updateQuery(query: Query, graph: PureModel): Promise { // TODO: improve abstraction so that we can get the current user ID from any abstract engine - const currentUserId = - this.engine instanceof V1_RemoteEngine - ? this.engine.getCurrentUserId() - : undefined; return V1_buildQuery( await this.engine.updateQuery(V1_transformQuery(query)), graph, - currentUserId, + this.engine.getCurrentUserId(), ); } async patchQuery(query: Partial, graph: PureModel): Promise { // TODO: improve abstraction so that we can get the current user ID from any abstract engine - const currentUserId = - this.engine instanceof V1_RemoteEngine - ? this.engine.getCurrentUserId() - : undefined; return V1_buildQuery( await this.engine.patchQuery(V1_transformQuery(query)), graph, - currentUserId, + this.engine.getCurrentUserId(), ); } @@ -3143,13 +3140,9 @@ export class V1_PureGraphManager extends AbstractPureGraphManager { const query = await this.engine.getQuery(queryId); query.name = queryName; // TODO: improve abstraction so that we can get the current user ID from any abstract engine - const currentUserId = - this.engine instanceof V1_RemoteEngine - ? this.engine.getCurrentUserId() - : undefined; return V1_buildLightQuery( await this.engine.updateQuery(query), - currentUserId, + this.engine.getCurrentUserId(), ); } @@ -3270,6 +3263,7 @@ export class V1_PureGraphManager extends AbstractPureGraphManager { : this.getFullGraphModelData(graph); return V1_buildModelCoverageAnalysisResult( await this.engine.analyzeMappingModelCoverage(input), + this, mapping, ); } @@ -3283,6 +3277,7 @@ export class V1_PureGraphManager extends AbstractPureGraphManager { V1_MappingModelCoverageAnalysisResult, input as PlainObject, ), + this, mapping, ); } diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/V1_EngineHelper.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/V1_EngineHelper.ts index 3b63bf8137..4a5f556ba2 100644 --- a/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/V1_EngineHelper.ts +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/V1_EngineHelper.ts @@ -42,7 +42,6 @@ import { type V1_QueryExecutionContext, } from './query/V1_Query.js'; import type { PureModel } from '../../../../../graph/PureModel.js'; -import { PackageableElementExplicitReference } from '../../../../../graph/metamodel/pure/packageableElements/PackageableElementReference.js'; import { DEPRECATED__ServiceTestResult } from '../../../../../graph-manager/action/service/DEPRECATED__ServiceTestResult.js'; import type { V1_DEPRECATED__ServiceTestResult } from './service/V1_DEPRECATED__ServiceTestResult.js'; import type { V1_ServiceRegistrationResult } from './service/V1_ServiceRegistrationResult.js'; @@ -84,6 +83,7 @@ import { ExternalFormatDescription } from '../../../../../graph-manager/action/e import type { Service } from '../../../../../graph/metamodel/pure/packageableElements/service/Service.js'; import { QUERY_PROFILE_PATH } from '../../../../../graph/MetaModelConst.js'; import { isValidFullPath } from '../../../../../graph/MetaModelUtils.js'; +import { PackageableElementExplicitReference } from '../../../../../graph/metamodel/pure/packageableElements/PackageableElementReference.js'; export const V1_buildLightQuery = ( protocol: V1_LightQuery, @@ -199,6 +199,9 @@ export const V1_buildExecutionContextInfo = ( if (dataspace) { const exec = new QueryDataSpaceExecutionContextInfo(); exec.dataSpacePath = dataspace; + if (v1_execContext instanceof V1_QueryDataSpaceExecutionContext) { + exec.executionKey = v1_execContext.executionKey; + } return exec; } else if (protocol.mapping && protocol.runtime) { const exec = new QueryExplicitExecutionContextInfo(); @@ -245,7 +248,6 @@ export const V1_buildQuery = ( protocol.artifactId, `Query 'artifactId' field is missing`, ); - metamodel.executionContext = V1_buildExecutionContext( protocol, graph, diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/V1_GraphManagerEngine.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/V1_GraphManagerEngine.ts index 61118b0f95..223438a941 100644 --- a/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/V1_GraphManagerEngine.ts +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/V1_GraphManagerEngine.ts @@ -335,6 +335,8 @@ export interface V1_GraphManagerEngine { cancelUserExecutions: (broadcastToCluster: boolean) => Promise; + getCurrentUserId: () => string | undefined; + // ------------------------------------------ Analysis ------------------------------------------ analyzeMappingModelCoverage: ( diff --git a/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/analytics/V1_MappingModelCoverageAnalysis.ts b/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/analytics/V1_MappingModelCoverageAnalysis.ts index d5bea9ca2e..301eded4be 100644 --- a/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/analytics/V1_MappingModelCoverageAnalysis.ts +++ b/packages/legend-graph/src/graph-manager/protocol/pure/v1/engine/analytics/V1_MappingModelCoverageAnalysis.ts @@ -33,11 +33,18 @@ import { EntityMappedProperty, EnumMappedProperty, MappedEntity, + MappedEntityInfo, MappedProperty, MappingModelCoverageAnalysisResult, } from '../../../../../../graph-manager/action/analytics/MappingModelCoverageAnalysis.js'; import type { V1_PureModelContext } from '../../model/context/V1_PureModelContext.js'; -import { V1_pureModelContextPropSchema } from '../../transformation/pureProtocol/V1_PureProtocolSerialization.js'; +import { + V1_deserializePureModelContextData, + V1_pureModelContextPropSchema, + V1_serializePureModelContextData, +} from '../../transformation/pureProtocol/V1_PureProtocolSerialization.js'; +import type { V1_PureModelContextData } from '../../model/context/V1_PureModelContextData.js'; +import type { V1_PureGraphManager } from '../../V1_PureGraphManager.js'; enum V1_MappedPropertyType { ENUM = 'enum', @@ -101,13 +108,31 @@ const V1_deserializeMappedProperty = ( } }; +class V1_MappedEntityInfo { + classPath!: string; + isRootEntity!: boolean; + subClasses: string[] = []; + + static readonly serialization = new SerializationFactory( + createModelSchema(V1_MappedEntityInfo, { + classPath: primitive(), + isRootEntity: primitive(), + subClasses: list(primitive()), + }), + ); +} + class V1_MappedEntity { path!: string; properties: V1_MappedProperty[] = []; + info?: V1_MappedEntityInfo | undefined; static readonly serialization = new SerializationFactory( createModelSchema(V1_MappedEntity, { path: primitive(), + info: optional( + usingModelSchema(V1_MappedEntityInfo.serialization.schema), + ), properties: list( custom( (prop) => V1_serializeMappedProperty(prop), @@ -134,12 +159,19 @@ export class V1_MappingModelCoverageAnalysisInput { export class V1_MappingModelCoverageAnalysisResult { mappedEntities: V1_MappedEntity[] = []; + model?: V1_PureModelContextData | undefined; static readonly serialization = new SerializationFactory( createModelSchema(V1_MappingModelCoverageAnalysisResult, { mappedEntities: list( usingModelSchema(V1_MappedEntity.serialization.schema), ), + model: optional( + custom( + (val) => V1_serializePureModelContextData(val), + (val) => V1_deserializePureModelContextData(val), + ), + ), }), ); } @@ -155,17 +187,35 @@ const buildMappedProperty = (protocol: V1_MappedProperty): MappedProperty => ? new EnumMappedProperty(protocol.name, protocol.enumPath) : new MappedProperty(protocol.name); -const buildMappedEntity = (protocol: V1_MappedEntity): MappedEntity => - new MappedEntity( +const buildMappedEntityInfo = ( + protocol: V1_MappedEntityInfo, +): MappedEntityInfo => + new MappedEntityInfo( + protocol.classPath, + protocol.isRootEntity, + protocol.subClasses, + ); + +const buildMappedEntity = (protocol: V1_MappedEntity): MappedEntity => { + const info = protocol.info ? buildMappedEntityInfo(protocol.info) : undefined; + return new MappedEntity( protocol.path, protocol.properties.map((p) => buildMappedProperty(p)), + info, ); +}; export const V1_buildModelCoverageAnalysisResult = ( protocol: V1_MappingModelCoverageAnalysisResult, + graphManager: V1_PureGraphManager, mapping: Mapping, -): MappingModelCoverageAnalysisResult => - new MappingModelCoverageAnalysisResult( +): MappingModelCoverageAnalysisResult => { + const entities = protocol.model?.elements.map((el) => + graphManager.elementProtocolToEntity(el), + ); + return new MappingModelCoverageAnalysisResult( protocol.mappedEntities.map((p) => buildMappedEntity(p)), mapping, + entities, ); +}; diff --git a/packages/legend-graph/src/index.ts b/packages/legend-graph/src/index.ts index b8ea187903..d1e22dc402 100644 --- a/packages/legend-graph/src/index.ts +++ b/packages/legend-graph/src/index.ts @@ -162,6 +162,7 @@ export { V1_buildExecutionError, V1_buildParserError, } from './graph-manager/protocol/pure/v1/engine/V1_EngineHelper.js'; +export { V1_buildFunctionInfoAnalysis } from './graph-manager/protocol/pure/v1/helpers/V1_DomainHelper.js'; export { V1_buildExecutionResult, V1_serializeExecutionResult, @@ -182,6 +183,7 @@ export { V1_ColSpec } from './graph-manager/protocol/pure/v1/model/valueSpecific export { V1_ColSpecArray } from './graph-manager/protocol/pure/v1/model/valueSpecification/raw/classInstance/relation/V1_ColSpecArray.js'; export { V1_PrimitiveValueSpecification } from './graph-manager/protocol/pure/v1/model/valueSpecification/raw/V1_PrimitiveValueSpecification.js'; export { V1_INTERNAL__UnknownFunctionActivator } from './graph-manager/protocol/pure/v1/model/packageableElements/function/V1_INTERNAL__UnknownFunctionActivator.js'; +export { V1_ConcreteFunctionDefinition } from './graph-manager/protocol/pure/v1/model/packageableElements/function/V1_ConcreteFunctionDefinition.js'; export { V1_GraphFetchTree, type V1_GraphFetchTreeVisitor, diff --git a/packages/legend-query-builder/src/components/QueryLoader.tsx b/packages/legend-query-builder/src/components/QueryLoader.tsx index 2a8ed2b68c..76e51d11b1 100644 --- a/packages/legend-query-builder/src/components/QueryLoader.tsx +++ b/packages/legend-query-builder/src/components/QueryLoader.tsx @@ -572,10 +572,12 @@ export const QueryLoader = observer( title={`Click to ${loadActionLabel}...`} key={templateQuery.title} onClick={() => { - loadCuratedTemplateQuery()( - templateQuery, - guaranteeNonNullable( - queryLoaderState.queryBuilderState, + flowResult( + loadCuratedTemplateQuery()( + templateQuery, + guaranteeNonNullable( + queryLoaderState.queryBuilderState, + ), ), ); queryLoaderState.setQueryLoaderDialogOpen(false); diff --git a/packages/legend-query-builder/src/components/workflows/ServiceQueryBuilder.tsx b/packages/legend-query-builder/src/components/workflows/ServiceQueryBuilder.tsx index fba5cdb413..88e68818b6 100644 --- a/packages/legend-query-builder/src/components/workflows/ServiceQueryBuilder.tsx +++ b/packages/legend-query-builder/src/components/workflows/ServiceQueryBuilder.tsx @@ -31,6 +31,7 @@ import { buildElementOption, type PackageableElementOption, } from '@finos/legend-lego/graph-editor'; +import { flowResult } from 'mobx'; type ExecutionContextOption = { label: string; @@ -82,16 +83,21 @@ const ServiceQueryBuilderSetupPanelContent = observer( queryBuilderState.selectedExecutionContext, ) : null; - const onExecutionContextOptionChange = ( + const onExecutionContextOptionChange = async ( option: ExecutionContextOption, - ): void => { + ): Promise => { if (option.value === queryBuilderState.selectedExecutionContext) { return; } queryBuilderState.setSelectedExecutionContext(option.value); - queryBuilderState.propagateExecutionContextChange(option.value); + await queryBuilderState.propagateExecutionContextChange(); queryBuilderState.onExecutionContextChange?.(option.value); }; + const handleExecutionContextOptionChange = ( + option: ExecutionContextOption, + ): void => { + flowResult(onExecutionContextOptionChange(option)); + }; // class const classes = queryBuilderState.executionContextState.mapping @@ -151,7 +157,7 @@ const ServiceQueryBuilderSetupPanelContent = observer( (executionContextOptions.length === 1 && Boolean(selectedExecutionContextOption)) } - onChange={onExecutionContextOptionChange} + onChange={handleExecutionContextOptionChange} value={selectedExecutionContextOption} darkMode={ !applicationStore.layoutService diff --git a/packages/legend-query-builder/src/stores/QueryBuilderState.ts b/packages/legend-query-builder/src/stores/QueryBuilderState.ts index b8c206afe7..67ef00b527 100644 --- a/packages/legend-query-builder/src/stores/QueryBuilderState.ts +++ b/packages/legend-query-builder/src/stores/QueryBuilderState.ts @@ -69,10 +69,10 @@ import { buildLambdaVariableExpressions, buildRawLambdaFromLambdaFunction, PrimitiveType, - PackageableElementExplicitReference, RuntimePointer, QueryExplicitExecutionContext, attachFromQuery, + PackageableElementExplicitReference, } from '@finos/legend-graph'; import { buildLambdaFunction } from './QueryBuilderValueSpecificationBuilder.js'; import type { @@ -108,6 +108,7 @@ import { QUERY_BUILDER_EVENT } from '../__lib__/QueryBuilderEvent.js'; import { QUERY_BUILDER_SETTING_KEY } from '../__lib__/QueryBuilderSetting.js'; import { QueryBuilderChangeHistoryState } from './QueryBuilderChangeHistoryState.js'; import { type QueryBuilderWorkflowState } from './query-workflow/QueryBuilderWorkFlowState.js'; +import type { QueryBuilder_LegendApplicationPlugin_Extension } from './QueryBuilder_LegendApplicationPlugin_Extension.js'; // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface QueryableSourceInfo {} @@ -350,6 +351,26 @@ export abstract class QueryBuilderState implements CommandRegistrar { return queryExeContext; } + async propagateExecutionContextChange( + isGraphBuildingNotRequired?: boolean, + ): Promise { + const propagateFuncHelpers = this.applicationStore.pluginManager + .getApplicationPlugins() + .flatMap( + (plugin) => + ( + plugin as QueryBuilder_LegendApplicationPlugin_Extension + ).getExtraQueryBuilderPropagateExecutionContextChangeHelper?.() ?? [], + ); + for (const helper of propagateFuncHelpers) { + const propagateFuncHelper = helper(this, isGraphBuildingNotRequired); + if (propagateFuncHelper) { + await propagateFuncHelper(); + return; + } + } + } + /** * Gets information about the current queryBuilderState. * This information can be used as a part of analytics diff --git a/packages/legend-query-builder/src/stores/QueryBuilder_LegendApplicationPlugin_Extension.ts b/packages/legend-query-builder/src/stores/QueryBuilder_LegendApplicationPlugin_Extension.ts index ac356dc94d..4d5b10be07 100644 --- a/packages/legend-query-builder/src/stores/QueryBuilder_LegendApplicationPlugin_Extension.ts +++ b/packages/legend-query-builder/src/stores/QueryBuilder_LegendApplicationPlugin_Extension.ts @@ -37,7 +37,7 @@ export type CuratedTemplateQuerySpecification = { loadCuratedTemplateQuery( templateQuery: CuratedTemplateQuery, queryBuilderState: QueryBuilderState, - ): void; + ): Promise; }; export type LoadQueryFilterOption = { @@ -95,6 +95,11 @@ export type QueryBuilderMenuActionConfiguration = { ) => React.ReactNode; }; +export type QueryBuilderPropagateExecutionContextChangeHelper = ( + queryBuilderState: QueryBuilderState, + isGraphBuildingNotRequired?: boolean, +) => (() => Promise) | undefined; + export interface QueryBuilder_LegendApplicationPlugin_Extension extends LegendApplicationPlugin { /** @@ -158,4 +163,9 @@ export interface QueryBuilder_LegendApplicationPlugin_Extension * Get the list of export menu action configurations */ getExtraQueryBuilderExportMenuActionConfigurations?(): QueryBuilderMenuActionConfiguration[]; + + /** + * Get the list of Query Builder Propagate Execution Context Change Helper + */ + getExtraQueryBuilderPropagateExecutionContextChangeHelper?(): QueryBuilderPropagateExecutionContextChangeHelper[]; } diff --git a/packages/legend-query-builder/src/stores/workflows/ServiceQueryBuilderState.ts b/packages/legend-query-builder/src/stores/workflows/ServiceQueryBuilderState.ts index eed1c857f0..ff36165d9f 100644 --- a/packages/legend-query-builder/src/stores/workflows/ServiceQueryBuilderState.ts +++ b/packages/legend-query-builder/src/stores/workflows/ServiceQueryBuilderState.ts @@ -156,23 +156,25 @@ export class ServiceQueryBuilderState extends QueryBuilderState { * - If no class is chosen, try to choose a compatible class * - If the chosen class is compatible with the new chosen mapping, do nothing, otherwise, try to choose a compatible class */ - propagateExecutionContextChange( - executionContext: ServiceExecutionContext, - ): void { - const mapping = executionContext.mapping; - this.changeMapping(mapping, { keepQueryContent: true }); - this.changeRuntime(executionContext.runtimeValue); - - const compatibleClasses = getMappingCompatibleClasses( - mapping, - this.graphManagerState.usableClasses, - ); - // if there is no chosen class or the chosen one is not compatible - // with the mapping then pick a compatible class if possible - if (!this.class || !compatibleClasses.includes(this.class)) { - const possibleNewClass = getNullableFirstEntry(compatibleClasses); - if (possibleNewClass) { - this.changeClass(possibleNewClass); + override async propagateExecutionContextChange( + isGraphBuildingNotRequired?: boolean, + ): Promise { + if (this.selectedExecutionContext) { + const mapping = this.selectedExecutionContext.mapping; + this.changeMapping(mapping, { keepQueryContent: true }); + this.changeRuntime(this.selectedExecutionContext.runtimeValue); + + const compatibleClasses = getMappingCompatibleClasses( + mapping, + this.graphManagerState.usableClasses, + ); + // if there is no chosen class or the chosen one is not compatible + // with the mapping then pick a compatible class if possible + if (!this.class || !compatibleClasses.includes(this.class)) { + const possibleNewClass = getNullableFirstEntry(compatibleClasses); + if (possibleNewClass) { + this.changeClass(possibleNewClass); + } } } }