UNPKG

@finos/legend-extension-dsl-data-space-studio

Version:
625 lines (596 loc) 21.9 kB
/** * Copyright (c) 2020-present, Goldman Sachs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { type LightQuery, type QueryInfo } from '@finos/legend-graph'; import { type DepotServerClient, StoreProjectData, ProjectDependencyCoordinates, ProjectVersionEntities, MASTER_SNAPSHOT_ALIAS, } from '@finos/legend-server-depot'; import { ActionState, assertErrorThrown, guaranteeNonNullable, LogEvent, type GeneratorFn, type PlainObject, } from '@finos/legend-shared'; import { type Entity } from '@finos/legend-storage'; import { type ProjectConfigurationStatus, fetchProjectConfigurationStatus, generateEditorRoute, LEGEND_STUDIO_APP_EVENT, type LegendStudioApplicationStore, EditorStore, generateReviewRoute, } from '@finos/legend-application-studio'; import { DEFAULT_TAB_SIZE, ActionAlertType, ActionAlertActionType, } from '@finos/legend-application'; import { makeObservable, observable, computed, action, flow, flowResult, } from 'mobx'; import { type SDLCServerClient, type ProjectDependency, Project, WorkspaceType, Workspace, ProjectConfiguration, EntityChangeType, } from '@finos/legend-server-sdlc'; import { DATA_SPACE_ELEMENT_CLASSIFIER_PATH, DSL_DataSpace_getGraphManagerExtension, type DSL_DataSpace_PureGraphManagerExtension, } from '@finos/legend-extension-dsl-data-space/graph'; import { generateDataSpaceTemplateQueryPromotionRoute } from '@finos/legend-extension-dsl-data-space/application'; const projectDependencyToProjectCoordinates = ( projectDependency: ProjectDependency, ): ProjectDependencyCoordinates => new ProjectDependencyCoordinates( guaranteeNonNullable(projectDependency.groupId), guaranteeNonNullable(projectDependency.artifactId), projectDependency.versionId, ); const DEFAULT_WORKSPACE_NAME_PREFIX = 'promote-as-template-query'; export class DataSpaceTemplateQueryPromotionReviewerStore { readonly applicationStore: LegendStudioApplicationStore; readonly sdlcServerClient: SDLCServerClient; readonly depotServerClient: DepotServerClient; readonly initState = ActionState.create(); readonly promoteState = ActionState.create(); readonly loadQueryState = ActionState.create(); readonly loadWorkspacesState = ActionState.create(); editorStore: EditorStore; graphManagerExtension: DSL_DataSpace_PureGraphManagerExtension; currentQuery?: LightQuery | undefined; currentQueryInfo?: QueryInfo | undefined; currentQueryProject?: StoreProjectData | undefined; currentProject?: Project | undefined; currentProjectConfiguration?: ProjectConfiguration; currentProjectConfigurationStatus?: ProjectConfigurationStatus | undefined; currentProjectEntities: Entity[] = []; dependencyEntities: Entity[] = []; groupWorkspaces: Workspace[] = []; workspaceName = ''; dataSpacePath!: string; dataSpaceEntity: Entity | undefined; templateQueryId = 'template_id'; templateQueryTitle = 'template_title'; templateQueryDescription = ''; constructor( applicationStore: LegendStudioApplicationStore, sdlcServerClient: SDLCServerClient, depotServerClient: DepotServerClient, ) { makeObservable(this, { editorStore: observable, graphManagerExtension: observable, currentQuery: observable, currentQueryInfo: observable, currentQueryProject: observable, currentProject: observable, currentProjectConfiguration: observable, currentProjectConfigurationStatus: observable, currentProjectEntities: observable, dataSpaceEntity: observable, groupWorkspaces: observable, workspaceName: observable, templateQueryId: observable, templateQueryTitle: observable, templateQueryDescription: observable, isWorkspaceNameValid: computed, isTemplateQueryIdValid: computed, setWorkspaceName: action, setTemplateQueryId: action, setTemplateQueryTitle: action, setTemplateQueryDescription: action, initialize: flow, loadQuery: flow, loadProject: flow, promoteAsTemplateQuery: flow, }); this.applicationStore = applicationStore; this.sdlcServerClient = sdlcServerClient; this.depotServerClient = depotServerClient; this.editorStore = new EditorStore( applicationStore, sdlcServerClient, depotServerClient, ); this.graphManagerExtension = DSL_DataSpace_getGraphManagerExtension( this.editorStore.graphManagerState.graphManager, ); } setWorkspaceName(val: string): void { this.workspaceName = val; } setTemplateQueryId(val: string): void { this.templateQueryId = val; } setTemplateQueryTitle(val: string): void { this.templateQueryTitle = val; } setTemplateQueryDescription(val: string): void { this.templateQueryDescription = val; } get isWorkspaceNameValid(): boolean { return !this.groupWorkspaces.some( (ws) => ws.workspaceId === this.workspaceName, ); } get isTemplateQueryIdValid(): boolean { if (this.dataSpaceEntity) { return this.graphManagerExtension.IsTemplateQueryIdValid( this.dataSpaceEntity, this.templateQueryId, ); } return false; } *initialize( queryId: string | undefined, dataSpacePath: string, ): GeneratorFn<void> { if (!this.initState.isInInitialState) { return; } try { this.initState.inProgress(); yield this.graphManagerExtension.graphManager.initialize( { env: this.applicationStore.config.env, tabSize: DEFAULT_TAB_SIZE, clientConfig: { baseUrl: this.applicationStore.config.engineServerUrl, queryBaseUrl: this.applicationStore.config.engineQueryServerUrl, enableCompression: true, }, }, { tracerService: this.applicationStore.tracerService, }, ); this.dataSpacePath = dataSpacePath; if (queryId) { let query: LightQuery | undefined; try { query = (yield this.graphManagerExtension.graphManager.getLightQuery( queryId, )) as LightQuery; } catch { query = undefined; } if (query) { yield flowResult(this.loadQuery(query)); } else { this.applicationStore.notificationService.notifyError( `Unable to find query with ID: ${queryId}`, ); } } if (this.currentQuery) { this.currentQueryProject = StoreProjectData.serialization.fromJson( (yield this.depotServerClient.getProject( this.currentQuery.groupId, this.currentQuery.artifactId, )) as PlainObject<StoreProjectData>, ); const projectData = (yield Promise.all([ this.depotServerClient.getVersionEntities( this.currentQuery.groupId, this.currentQuery.artifactId, MASTER_SNAPSHOT_ALIAS, ), this.sdlcServerClient.getConfiguration( this.currentQueryProject.projectId, undefined, ), ])) as [Entity[], PlainObject<ProjectConfiguration>]; const [currentProjectEntities, currentProjectConfiguration] = [ projectData[0], ProjectConfiguration.serialization.fromJson(projectData[1]), ]; this.currentProjectConfiguration = currentProjectConfiguration; const dependencyEntities = ( (yield this.depotServerClient.collectDependencyEntities( ( [ ...currentProjectConfiguration.projectDependencies, ] as ProjectDependency[] ) .map(projectDependencyToProjectCoordinates) .map((p) => ProjectDependencyCoordinates.serialization.toJson(p)), true, true, )) as PlainObject<ProjectVersionEntities>[] ) .map((p) => ProjectVersionEntities.serialization.fromJson(p)) .flatMap((info) => info.entities); this.dependencyEntities = dependencyEntities; this.currentProjectEntities = currentProjectEntities; this.dataSpaceEntity = guaranteeNonNullable( currentProjectEntities.filter( (entity: Entity) => entity.path === dataSpacePath && entity.classifierPath === DATA_SPACE_ELEMENT_CLASSIFIER_PATH, )[0], `Can't find data product entity with path ${this.dataSpaceEntity}`, ); this.initState.pass(); } } catch (error) { assertErrorThrown(error); this.applicationStore.logService.error( LogEvent.create(LEGEND_STUDIO_APP_EVENT.GENERIC_FAILURE), error, ); this.applicationStore.alertService.setBlockingAlert({ message: `Can't initialize template query promotion reviewer store`, }); this.initState.fail(); } } *loadQuery(query: LightQuery): GeneratorFn<void> { this.currentQuery = query; this.templateQueryTitle = query.name; try { this.loadQueryState.inProgress(); this.currentQueryInfo = (yield this.graphManagerExtension.graphManager.getQueryInfo( query.id, )) as QueryInfo; this.currentQueryProject = StoreProjectData.serialization.fromJson( (yield this.depotServerClient.getProject( this.currentQuery.groupId, this.currentQuery.artifactId, )) as PlainObject<StoreProjectData>, ); const updatedQueryName = query.name.replace(/[^a-zA-Z0-9]/g, ''); this.setWorkspaceName( `${DEFAULT_WORKSPACE_NAME_PREFIX}-${updatedQueryName}`, ); this.applicationStore.navigationService.navigator.updateCurrentLocation( generateDataSpaceTemplateQueryPromotionRoute( this.currentQuery.groupId, this.currentQuery.artifactId, this.currentQuery.versionId, this.dataSpacePath, query.id, ), ); const currentProject = Project.serialization.fromJson( (yield this.sdlcServerClient.getProject( this.currentQueryProject.projectId, )) as PlainObject<Project>, ); yield flowResult(this.loadProject(currentProject)); } catch (error) { assertErrorThrown(error); this.applicationStore.notificationService.notifyError(error); } finally { this.loadQueryState.complete(); } } *loadProject(project: Project): GeneratorFn<void> { this.currentProject = project; this.currentProjectConfigurationStatus = undefined; this.loadWorkspacesState.inProgress(); try { this.currentProjectConfigurationStatus = (yield fetchProjectConfigurationStatus( project.projectId, undefined, this.applicationStore, this.sdlcServerClient, )) as ProjectConfigurationStatus; this.groupWorkspaces = ( (yield this.sdlcServerClient.getGroupWorkspaces( project.projectId, )) as PlainObject<Workspace>[] ) .map((v) => Workspace.serialization.fromJson(v)) .filter((workspace) => workspace.workspaceType === WorkspaceType.GROUP); this.loadWorkspacesState.pass(); } catch (error) { assertErrorThrown(error); this.applicationStore.logService.error( LogEvent.create(LEGEND_STUDIO_APP_EVENT.SDLC_MANAGER_FAILURE), error, ); this.applicationStore.notificationService.notifyError(error); this.loadWorkspacesState.fail(); } } *promoteAsTemplateQuery(): GeneratorFn<void> { const query = this.currentQuery; const project = this.currentProject; if ( this.promoteState.isInProgress || !query || !this.currentQueryInfo || !this.currentProjectConfiguration || !project || !this.workspaceName || !this.templateQueryTitle || !this.dataSpaceEntity || !this.isWorkspaceNameValid || !this.isTemplateQueryIdValid ) { return; } try { this.promoteState.inProgress(); // 1. prepare project entities this.applicationStore.alertService.setBlockingAlert({ message: `Fetching and updating project...`, prompt: 'Please do not close the application', showLoading: true, }); // update datasapce entity const updatedDataSpaceEntity = (yield this.graphManagerExtension.addNewExecutableToDataSpaceEntity( this.dataSpaceEntity, this.currentQueryInfo, { id: this.templateQueryId, title: this.templateQueryTitle, description: this.templateQueryDescription, }, )) as Entity; guaranteeNonNullable( this.currentProjectEntities.filter( (entity: Entity) => entity.path === this.dataSpacePath && entity.classifierPath === DATA_SPACE_ELEMENT_CLASSIFIER_PATH, )[0], ).content = updatedDataSpaceEntity.content; // 2. check if the graph compiles properly this.applicationStore.alertService.setBlockingAlert({ message: `Checking workspace compilation status...`, prompt: 'Please do not close the application', showLoading: true, }); let compilationFailed = false; try { yield this.graphManagerExtension.graphManager.compileEntities([ ...this.dependencyEntities, ...this.currentProjectEntities, ]); } catch { compilationFailed = true; } // 3. proceed to setup the workspace const setupWorkspace = async (): Promise<void> => { let workspace: Workspace | undefined; try { this.applicationStore.alertService.setBlockingAlert({ message: `Creating workspace...`, prompt: 'Please do not close the application', showLoading: true, }); // i. create workspace workspace = Workspace.serialization.fromJson( await this.sdlcServerClient.createWorkspace( project.projectId, undefined, this.workspaceName, WorkspaceType.GROUP, ), ); // ii. update data product this.applicationStore.alertService.setBlockingAlert({ message: `Generating code commit...`, prompt: 'Please do not close the application', showLoading: true, }); await this.sdlcServerClient.performEntityChanges( project.projectId, workspace, { message: 'promote-as-template-query: promote query as a template query to data product', entityChanges: [ { classifierPath: updatedDataSpaceEntity.classifierPath, entityPath: updatedDataSpaceEntity.path, content: updatedDataSpaceEntity.content, type: EntityChangeType.MODIFY, }, ], }, ); // iii create review this.applicationStore.alertService.setBlockingAlert({ message: `Generating code review...`, prompt: 'Please do not close the application', showLoading: true, }); await flowResult( this.editorStore.initialize( project.projectId, undefined, workspace.workspaceId, workspace.workspaceType, undefined, ), ); const workspaceReviewState = this.editorStore.workspaceReviewState; const workspaceContainsSnapshotDependencies = this.editorStore.projectConfigurationEditorState .containsSnapshotDependencies; const isCreateReviewDisabled = Boolean(workspaceReviewState.workspaceReview) || workspaceContainsSnapshotDependencies || !workspaceReviewState.canCreateReview || workspaceReviewState.sdlcState.isActiveProjectSandbox; workspaceReviewState.reviewTitle = 'code review - promote query as a template query to data product'; if (!isCreateReviewDisabled) { await flowResult( workspaceReviewState.createWorkspaceReview( workspaceReviewState.reviewTitle, ), ); } else { this.applicationStore.notificationService.notifyError( `Can't create code review`, ); } // iv. complete, redirect user to the service query editor screen this.applicationStore.alertService.setBlockingAlert(undefined); this.promoteState.pass(); this.applicationStore.alertService.setActionAlertInfo({ message: `Successfully promoted query into data product '${this.dataSpacePath}'. Now your template query can be found in workspace '${this.workspaceName}' of project '${project.name}' (${project.projectId})`, prompt: compilationFailed ? `The workspace might not compile at the moment, please make sure to fix the issue and submit a review to make the data product part of the project to complete template query promotion` : `Please make sure to get the generated code-review reviewed and approved`, type: ActionAlertType.STANDARD, actions: compilationFailed ? [ { label: 'Open Workspace', type: ActionAlertActionType.PROCEED, handler: (): void => { this.applicationStore.navigationService.navigator.goToLocation( generateEditorRoute( project.projectId, undefined, this.workspaceName, WorkspaceType.GROUP, ), ); }, default: true, }, ] : [ { label: 'Open Code Review', type: ActionAlertActionType.PROCEED, handler: (): void => { if (workspaceReviewState.workspaceReview) { this.applicationStore.navigationService.navigator.visitAddress( this.applicationStore.navigationService.navigator.generateAddress( generateReviewRoute( workspaceReviewState.workspaceReview.projectId, workspaceReviewState.workspaceReview.id, ), ), ); } }, default: true, }, { label: 'Open Workspace', type: ActionAlertActionType.PROCEED, handler: (): void => { this.applicationStore.navigationService.navigator.goToLocation( generateEditorRoute( project.projectId, undefined, this.workspaceName, WorkspaceType.GROUP, ), ); }, }, ], }); } catch (error) { assertErrorThrown(error); this.applicationStore.alertService.setBlockingAlert(undefined); this.applicationStore.notificationService.notifyError( `Can't set up workspace: ${error.message}`, ); if (workspace) { await this.sdlcServerClient.deleteWorkspace( project.projectId, workspace, ); } this.promoteState.fail(); } }; this.applicationStore.alertService.setBlockingAlert(undefined); if (compilationFailed) { this.applicationStore.alertService.setActionAlertInfo({ message: `We have found compilation issues with the workspace. Your query can still be promoted, but you would need to fix compilation issues afterwards`, prompt: `Do you still want to proceed to promote the query as a template query?`, type: ActionAlertType.STANDARD, actions: [ { label: `Proceed`, type: ActionAlertActionType.PROCEED_WITH_CAUTION, handler: (): void => { setupWorkspace().catch( this.applicationStore.alertUnhandledError, ); }, }, { label: 'Abort', type: ActionAlertActionType.PROCEED, handler: (): void => { this.promoteState.fail(); }, default: true, }, ], }); } else { yield setupWorkspace(); } this.promoteState.pass(); } catch (error) { assertErrorThrown(error); this.applicationStore.alertService.setBlockingAlert(undefined); this.applicationStore.notificationService.notifyError(error); this.promoteState.fail(); } } }