@finos/legend-application-studio
Version:
Legend Studio application core
603 lines (560 loc) • 19 kB
text/typescript
/**
* 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,
);
}
}
}