UNPKG

@finos/legend-application-studio

Version:
603 lines (560 loc) 19 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 { computed, observable, action, makeObservable, flow, flowResult, override, } from 'mobx'; import type { EditorStore } from '../../EditorStore.js'; import { type GeneratorFn, assertErrorThrown, LogEvent, guaranteeType, assertType, StopWatch, filterByType, assertTrue, } from '@finos/legend-shared'; import { ElementEditorState } from './ElementEditorState.js'; import { type CompilationError, type PackageableElement, type ExecutionResult, type RawExecutionPlan, type ExecutionResultWithMetadata, GRAPH_MANAGER_EVENT, LAMBDA_PIPE, ParserError, ConcreteFunctionDefinition, RawLambda, buildSourceInformationSourceId, isStubbed_PackageableElement, reportGraphAnalytics, buildLambdaVariableExpressions, VariableExpression, observe_ValueSpecification, generateFunctionPrettyName, RawVariableExpression, type FunctionActivator, } from '@finos/legend-graph'; import { ExecutionPlanState, LambdaEditorState, LambdaParameterState, LambdaParametersState, PARAMETER_SUBMIT_ACTION, QUERY_BUILDER_EVENT, QueryBuilderTelemetryHelper, buildExecutionParameterValues, getExecutionQueryFromRawLambda, } from '@finos/legend-query-builder'; import { FunctionActivatorState } from './FunctionActivatorState.js'; import { FunctionTestableState } from './function-activator/testable/FunctionTestableState.js'; export enum FUNCTION_EDITOR_TAB { DEFINITION = 'DEFINITION', TAGGED_VALUES = 'TAGGED_VALUES', STEREOTYPES = 'STEREOTYPES', TEST_SUITES = 'TEST_SUITES', LAMBDAS = 'LAMBDAS', } export class FunctionDefinitionEditorState extends LambdaEditorState { readonly editorStore: EditorStore; readonly functionElement: ConcreteFunctionDefinition; isConvertingFunctionBodyToString = false; constructor( functionElement: ConcreteFunctionDefinition, editorStore: EditorStore, ) { super('', LAMBDA_PIPE); makeObservable(this, { functionElement: observable, isConvertingFunctionBodyToString: observable, }); this.functionElement = functionElement; this.editorStore = editorStore; } get lambdaId(): string { return buildSourceInformationSourceId([this.functionElement.path]); } *convertLambdaGrammarStringToObject(): GeneratorFn<void> { if (this.lambdaString) { try { const lambda = (yield this.editorStore.graphManagerState.graphManager.pureCodeToLambda( this.fullLambdaString, this.lambdaId, )) as RawLambda; this.setParserError(undefined); this.functionElement.expressionSequence = lambda.body as object[]; } catch (error) { assertErrorThrown(error); if (error instanceof ParserError) { this.setParserError(error); } this.editorStore.applicationStore.logService.error( LogEvent.create(GRAPH_MANAGER_EVENT.PARSING_FAILURE), error, ); } } else { this.clearErrors(); this.functionElement.expressionSequence = []; } } *convertLambdaObjectToGrammarString(options?: { pretty?: boolean | undefined; preserveCompilationError?: boolean | undefined; firstLoad?: boolean | undefined; }): GeneratorFn<void> { if (!isStubbed_PackageableElement(this.functionElement)) { this.isConvertingFunctionBodyToString = true; try { const lambdas = new Map<string, RawLambda>(); const functionLamba = new RawLambda( [], this.functionElement.expressionSequence, ); lambdas.set(this.lambdaId, functionLamba); const isolatedLambdas = (yield this.editorStore.graphManagerState.graphManager.lambdasToPureCode( lambdas, options?.pretty, )) as Map<string, string>; const grammarText = isolatedLambdas.get(this.lambdaId); if (grammarText) { let grammarString = this.extractLambdaString(grammarText); if ( this.functionElement.expressionSequence.length > 1 && grammarString.endsWith('}') ) { // The lambda object to string converter wraps the lambda inside a '{}' in the case where there are more than one expressions inside the function // causing a parsing error. To handle this we extract only whats inside the '{}' and add ';' to avoid error. grammarString = grammarString.slice(0, -1); grammarString = `${ grammarString.endsWith('\n') ? grammarString.slice(0, -1) : grammarString };`; } this.setLambdaString(grammarString); } else { this.setLambdaString(''); } // `firstLoad` flag is used in the first rendering of the function editor (in a `useEffect`) // This flag helps block editing while the JSON is converting to text and to avoid reseting parser/compiler error in reveal error if (!options?.firstLoad) { this.clearErrors({ preserveCompilationError: options?.preserveCompilationError, }); } this.isConvertingFunctionBodyToString = false; } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.logService.error( LogEvent.create(GRAPH_MANAGER_EVENT.PARSING_FAILURE), error, ); this.isConvertingFunctionBodyToString = false; } } else { this.clearErrors(); this.setLambdaString(''); } } } export class FunctionParametersState extends LambdaParametersState { readonly functionEditorState: FunctionEditorState; constructor(functionEditorState: FunctionEditorState) { super(); makeObservable(this, { parameterValuesEditorState: observable, parameterStates: observable, addParameter: action, removeParameter: action, openModal: action, build: action, setParameters: action, }); this.functionEditorState = functionEditorState; } openModal(lambda: RawLambda): void { this.parameterStates = this.build(lambda); this.parameterValuesEditorState.open( (): Promise<void> => flowResult(this.functionEditorState.runFunc()).catch( this.functionEditorState.editorStore.applicationStore .alertUnhandledError, ), PARAMETER_SUBMIT_ACTION.RUN, ); } build(lambda: RawLambda): LambdaParameterState[] { const parameters = buildLambdaVariableExpressions( lambda, this.functionEditorState.editorStore.graphManagerState, ) .map((parameter) => observe_ValueSpecification( parameter, this.functionEditorState.editorStore.changeDetectionState .observerContext, ), ) .filter(filterByType(VariableExpression)); const states = parameters.map((variable) => { const parmeterState = new LambdaParameterState( variable, this.functionEditorState.editorStore.changeDetectionState.observerContext, this.functionEditorState.editorStore.graphManagerState.graph, ); parmeterState.mockParameterValue(); return parmeterState; }); return states; } } export class FunctionEditorState extends ElementEditorState { readonly functionDefinitionEditorState: FunctionDefinitionEditorState; readonly activatorPromoteState: FunctionActivatorState; readonly functionTestableEditorState: FunctionTestableState; selectedTab: FUNCTION_EDITOR_TAB; isRunningFunc = false; isGeneratingPlan = false; executionResult?: ExecutionResult | undefined; // NOTE: stored as lossless JSON string executionPlanState: ExecutionPlanState; parametersState: FunctionParametersState; funcRunPromise: Promise<ExecutionResultWithMetadata> | undefined = undefined; constructor(editorStore: EditorStore, element: PackageableElement) { super(editorStore, element); makeObservable(this, { selectedTab: observable, isRunningFunc: observable, isGeneratingPlan: observable, executionResult: observable, executionPlanState: observable, label: override, functionElement: computed, setSelectedTab: action, reprocess: action, setExecutionResult: action, setIsRunningFunc: action, runFunc: flow, generatePlan: flow, handleRunFunc: flow, cancelFuncRun: flow, updateFunctionWithQuery: flow, }); assertType( element, ConcreteFunctionDefinition, 'Element inside function editor state must be a function', ); this.selectedTab = FUNCTION_EDITOR_TAB.DEFINITION; this.functionDefinitionEditorState = new FunctionDefinitionEditorState( element, this.editorStore, ); this.activatorPromoteState = new FunctionActivatorState(this); this.executionPlanState = new ExecutionPlanState( this.editorStore.applicationStore, this.editorStore.graphManagerState, ); this.parametersState = new FunctionParametersState(this); this.functionTestableEditorState = new FunctionTestableState(this); } override get label(): string { return generateFunctionPrettyName(this.functionElement, { fullPath: true, spacing: false, }); } get functionElement(): ConcreteFunctionDefinition { return guaranteeType( this.element, ConcreteFunctionDefinition, 'Element inside function editor state must be a function', ); } get activators(): FunctionActivator[] { const allActivators = this.editorStore.graphManagerState.graph.functionActivators; return allActivators.filter( (activator: FunctionActivator) => activator.function.value === this.element, ); } setSelectedTab(tab: FUNCTION_EDITOR_TAB): void { this.selectedTab = tab; } override revealCompilationError(compilationError: CompilationError): boolean { let revealed = false; try { if (compilationError.sourceInformation) { this.setSelectedTab(FUNCTION_EDITOR_TAB.DEFINITION); this.functionDefinitionEditorState.setCompilationError( compilationError, ); revealed = true; } } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.logService.warn( LogEvent.create(GRAPH_MANAGER_EVENT.COMPILATION_FAILURE), `Can't locate error`, error, ); } return revealed; } override clearCompilationError(): void { this.functionDefinitionEditorState.setCompilationError(undefined); } *updateFunctionWithQuery(val: RawLambda): GeneratorFn<void> { const lambdaParam = val.parameters ? (val.parameters as object[]) : []; const parameters = lambdaParam .map((param) => this.editorStore.graphManagerState.graphManager.buildRawValueSpecification( param, this.editorStore.graphManagerState.graph, ), ) .map((rawValueSpec) => guaranteeType(rawValueSpec, RawVariableExpression), ); assertTrue( Array.isArray(val.body), `Query body expected to be a list of expressions`, ); this.functionElement.expressionSequence = val.body as object[]; this.functionElement.parameters = parameters; yield flowResult( this.functionDefinitionEditorState.convertLambdaObjectToGrammarString({ pretty: true, firstLoad: true, }), ); } reprocess( newElement: ConcreteFunctionDefinition, editorStore: EditorStore, ): FunctionEditorState { const functionEditorState = new FunctionEditorState( editorStore, newElement, ); functionEditorState.selectedTab = this.selectedTab; return functionEditorState; } setIsRunningFunc(val: boolean): void { this.isRunningFunc = val; } setExecutionResult = (executionResult: ExecutionResult | undefined): void => { this.executionResult = executionResult; }; setFuncRunPromise = ( promise: Promise<ExecutionResultWithMetadata> | undefined, ): void => { this.funcRunPromise = promise; }; get bodyExpressionSequence(): RawLambda { return new RawLambda( this.functionElement.parameters.map((parameter) => this.editorStore.graphManagerState.graphManager.serializeRawValueSpecification( parameter, ), ), this.functionElement.expressionSequence, ); } *generatePlan(debug: boolean): GeneratorFn<void> { if (this.isGeneratingPlan) { return; } try { const expressionSequence = this.bodyExpressionSequence; this.isGeneratingPlan = true; let rawPlan: RawExecutionPlan; const stopWatch = new StopWatch(); const report = reportGraphAnalytics( this.editorStore.graphManagerState.graph, ); if (debug) { QueryBuilderTelemetryHelper.logEvent_ExecutionPlanDebugLaunched( this.editorStore.applicationStore.telemetryService, ); const debugResult = (yield this.editorStore.graphManagerState.graphManager.debugExecutionPlanGeneration( expressionSequence, undefined, undefined, this.editorStore.graphManagerState.graph, report, )) as { plan: RawExecutionPlan; debug: string }; rawPlan = debugResult.plan; this.executionPlanState.setDebugText(debugResult.debug); } else { QueryBuilderTelemetryHelper.logEvent_ExecutionPlanGenerationLaunched( this.editorStore.applicationStore.telemetryService, ); rawPlan = (yield this.editorStore.graphManagerState.graphManager.generateExecutionPlan( expressionSequence, undefined, undefined, this.editorStore.graphManagerState.graph, report, )) as object; } stopWatch.record(); try { this.executionPlanState.setRawPlan(rawPlan); const plan = this.editorStore.graphManagerState.graphManager.buildExecutionPlan( rawPlan, this.editorStore.graphManagerState.graph, ); this.executionPlanState.initialize(plan); } catch { // do nothing } stopWatch.record(QUERY_BUILDER_EVENT.BUILD_EXECUTION_PLAN__SUCCESS); // report report.timings = this.editorStore.applicationStore.timeService.finalizeTimingsRecord( stopWatch, report.timings, ); if (debug) { QueryBuilderTelemetryHelper.logEvent_ExecutionPlanDebugSucceeded( this.editorStore.applicationStore.telemetryService, report, ); } else { QueryBuilderTelemetryHelper.logEvent_ExecutionPlanGenerationSucceeded( this.editorStore.applicationStore.telemetryService, report, ); } } catch (error) { assertErrorThrown(error); this.editorStore.applicationStore.logService.error( LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE), error, ); this.editorStore.applicationStore.notificationService.notifyError(error); } finally { this.isGeneratingPlan = false; } } *handleRunFunc(): GeneratorFn<void> { if (this.isRunningFunc) { return; } const expressionSequence = this.bodyExpressionSequence; const parameters = (expressionSequence.parameters ?? []) as object[]; if (parameters.length) { this.parametersState.openModal(expressionSequence); } else { this.runFunc(); } } *runFunc(): GeneratorFn<void> { if (this.isRunningFunc) { return; } QueryBuilderTelemetryHelper.logEvent_QueryRunLaunched( this.editorStore.applicationStore.telemetryService, ); let promise; try { this.isRunningFunc = true; const stopWatch = new StopWatch(); const report = reportGraphAnalytics( this.editorStore.graphManagerState.graph, ); promise = this.editorStore.graphManagerState.graphManager.runQuery( getExecutionQueryFromRawLambda( this.bodyExpressionSequence, this.parametersState.parameterStates, this.editorStore.graphManagerState, ), undefined, undefined, this.editorStore.graphManagerState.graph, { useLosslessParse: false, parameterValues: buildExecutionParameterValues( this.parametersState.parameterStates, this.editorStore.graphManagerState, ), }, report, ); this.setFuncRunPromise(promise); const result = (yield promise) as ExecutionResultWithMetadata; if (this.funcRunPromise === promise) { this.setExecutionResult(result.executionResult); this.parametersState.setParameters([]); // report report.timings = this.editorStore.applicationStore.timeService.finalizeTimingsRecord( stopWatch, report.timings, ); QueryBuilderTelemetryHelper.logEvent_QueryRunSucceeded( this.editorStore.applicationStore.telemetryService, report, ); } } catch (error) { // When user cancels the query by calling the cancelQuery api, it will throw an execution failure error. // For now, we don't want to notify users about this failure. Therefore we check to ensure the promise is still the same one. // When cancelled the query, we set the queryRunPromise as undefined. if (this.funcRunPromise === promise) { assertErrorThrown(error); this.editorStore.applicationStore.logService.error( LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE), error, ); this.editorStore.applicationStore.notificationService.notifyError( error, ); } } finally { this.isRunningFunc = false; } } *cancelFuncRun(): GeneratorFn<void> { this.setIsRunningFunc(false); this.setFuncRunPromise(undefined); try { yield this.editorStore.graphManagerState.graphManager.cancelUserExecutions( true, ); } catch (error) { // Don't notify users about success or failure this.editorStore.applicationStore.logService.error( LogEvent.create(GRAPH_MANAGER_EVENT.EXECUTION_FAILURE), error, ); } } }