@finos/legend-application-studio
Version:
Legend Studio application core
623 lines (577 loc) • 22.6 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, flow, flowResult, makeObservable, observable } from 'mobx';
import type { EditorStore } from '../editor/EditorStore.js';
import {
type GeneratorFn,
type PlainObject,
assertErrorThrown,
AssertionError,
LogEvent,
IllegalStateError,
StopWatch,
} from '@finos/legend-shared';
import {
type ProjectViewerPathParams,
generateViewProjectByGAVRoute,
generateViewVersionRoute,
generateViewRevisionRoute,
generateViewProjectRoute,
} from '../../__lib__/LegendStudioNavigation.js';
import {
type Entity,
type ProjectGAVCoordinates,
type EntitiesWithOrigin,
parseGAVCoordinates,
} from '@finos/legend-storage';
import {
ProjectConfiguration,
Revision,
RevisionAlias,
Version,
Workspace,
} from '@finos/legend-server-sdlc';
import { LEGEND_STUDIO_APP_EVENT } from '../../__lib__/LegendStudioEvent.js';
import { DEFAULT_TAB_SIZE } from '@finos/legend-application';
import { resolveVersion, StoreProjectData } from '@finos/legend-server-depot';
import {
type WorkflowManagerState,
ProjectVersionWorkflowManagerState,
ProjectWorkflowManagerState,
} from '../editor/sidebar-state/WorkflowManagerState.js';
import {
GRAPH_MANAGER_EVENT,
DependencyGraphBuilderError,
GraphDataDeserializationError,
GraphBuilderError,
createGraphBuilderReport,
LegendSDLC,
} from '@finos/legend-graph';
import { GRAPH_EDITOR_MODE } from '../editor/EditorConfig.js';
import { LegendStudioTelemetryHelper } from '../../__lib__/LegendStudioTelemetryHelper.js';
import { payloadDebugger } from '../editor/panel-group/DevToolPanelState.js';
interface ProjectViewerGraphBuilderMaterial {
entities: Entity[];
dependencyEntitiesIndex: Map<string, EntitiesWithOrigin>;
}
export class ProjectViewerStore {
readonly editorStore: EditorStore;
currentRevision?: Revision | undefined;
latestVersion?: Version | undefined;
revision?: Revision | undefined;
version?: Version | undefined;
initialEntityPath?: string | undefined;
projectGAVCoordinates?: ProjectGAVCoordinates | undefined;
workflowManagerState: WorkflowManagerState | undefined;
constructor(editorStore: EditorStore) {
makeObservable<
ProjectViewerStore,
'initializeWithProjectInformation' | 'initializeWithGAV'
>(this, {
currentRevision: observable,
latestVersion: observable,
revision: observable,
version: observable,
projectGAVCoordinates: observable.ref,
workflowManagerState: observable.ref,
onLatestVersion: computed,
onCurrentRevision: computed,
initializeWithProjectInformation: flow,
initializeWithGAV: flow,
buildGraph: flow,
initialize: flow,
});
this.editorStore = editorStore;
}
get onLatestVersion(): boolean {
return Boolean(
this.latestVersion && this.version && this.latestVersion === this.version,
);
}
get onCurrentRevision(): boolean {
return Boolean(
this.currentRevision &&
this.revision &&
this.currentRevision === this.revision,
);
}
/**
* Since we don't dynamically change the route based on the currently opened element
* We have to handle the following cases:
* 1. if the element is found and then the user opens another element
* 2. if the elemnt is not found
* in either case, the most suitable behavior at the moment is to internalize/swallow up the entity path param
*/
internalizeEntityPath(params: ProjectViewerPathParams): void {
const { gav, projectId, revisionId, versionId, entityPath } = params;
if (entityPath) {
this.initialEntityPath = entityPath;
if (projectId) {
this.editorStore.applicationStore.navigationService.navigator.updateCurrentLocation(
versionId
? generateViewVersionRoute(projectId, versionId)
: revisionId
? generateViewRevisionRoute(projectId, revisionId)
: generateViewProjectRoute(projectId),
);
} else if (gav) {
const {
groupId,
artifactId,
versionId: _versionId,
} = parseGAVCoordinates(gav);
this.editorStore.applicationStore.navigationService.navigator.updateCurrentLocation(
generateViewProjectByGAVRoute(groupId, artifactId, _versionId),
);
}
}
}
/**
* Initialize the graph by fetching project information from the SDLC server.
*/
private *initializeWithProjectInformation(
projectId: string,
versionId: string | undefined,
revisionId: string | undefined,
): GeneratorFn<ProjectViewerGraphBuilderMaterial> {
const stopWatch = new StopWatch();
// fetch project informations
this.editorStore.initState.setMessage(`Fetching project information...`);
yield flowResult(this.editorStore.sdlcState.fetchCurrentProject(projectId));
const stubWorkspace = new Workspace();
stubWorkspace.projectId = projectId;
stubWorkspace.workspaceId = '';
this.editorStore.sdlcState.setCurrentWorkspace(stubWorkspace);
// get current revision so we can show how "outdated" the `current view` of the project is
this.currentRevision = Revision.serialization.fromJson(
(yield this.editorStore.sdlcServerClient.getRevision(
this.editorStore.sdlcState.activeProject.projectId,
undefined,
RevisionAlias.CURRENT,
)) as PlainObject<Revision>,
);
this.latestVersion = Version.serialization.fromJson(
(yield this.editorStore.sdlcServerClient.getLatestVersion(
this.editorStore.sdlcState.activeProject.projectId,
)) as PlainObject<Version>,
);
// ensure only either version or revision is specified
if (versionId && revisionId) {
throw new IllegalStateError(
`Can't have both version ID and revision ID specified for viewer mode`,
);
}
let graphBuildingMaterial: [Entity[], PlainObject<ProjectConfiguration>];
this.editorStore.initState.setMessage(undefined);
// fetch entities
stopWatch.record();
this.editorStore.initState.setMessage(`Fetching entities...`);
if (versionId && !revisionId) {
// get version info if a version is specified
this.version =
versionId !== this.latestVersion.id.id
? Version.serialization.fromJson(
(yield this.editorStore.sdlcServerClient.getVersion(
this.editorStore.sdlcState.activeProject.projectId,
versionId,
)) as PlainObject<Version>,
)
: this.latestVersion;
graphBuildingMaterial = (yield Promise.all([
this.editorStore.sdlcServerClient.getEntitiesByVersion(
this.editorStore.sdlcState.activeProject.projectId,
versionId,
),
this.editorStore.sdlcServerClient.getConfigurationByVersion(
this.editorStore.sdlcState.activeProject.projectId,
versionId,
),
])) as [Entity[], PlainObject<ProjectConfiguration>];
} else if (revisionId && !versionId) {
// get revision info if a revision is specified
this.revision =
revisionId !== this.currentRevision.id
? Revision.serialization.fromJson(
(yield this.editorStore.sdlcServerClient.getRevision(
this.editorStore.sdlcState.activeProject.projectId,
undefined,
revisionId,
)) as PlainObject<Revision>,
)
: this.currentRevision;
graphBuildingMaterial = (yield Promise.all([
this.editorStore.sdlcServerClient.getEntitiesByRevision(
this.editorStore.sdlcState.activeProject.projectId,
undefined,
revisionId,
),
this.editorStore.sdlcServerClient.getConfigurationByRevision(
this.editorStore.sdlcState.activeProject.projectId,
undefined,
revisionId,
),
])) as [Entity[], PlainObject<ProjectConfiguration>];
}
// if no revision ID or version ID is specified, we will just get the project HEAD
else if (!revisionId && !versionId) {
graphBuildingMaterial = (yield Promise.all([
this.editorStore.sdlcServerClient.getEntities(
this.editorStore.sdlcState.activeProject.projectId,
undefined,
),
this.editorStore.sdlcServerClient.getConfiguration(
this.editorStore.sdlcState.activeProject.projectId,
undefined,
),
])) as [Entity[], PlainObject<ProjectConfiguration>];
} else {
throw new IllegalStateError(
`Can't initialize viewer when both 'verisonId' and 'revisionId' are provided`,
);
}
this.editorStore.initState.setMessage(undefined);
stopWatch.record(GRAPH_MANAGER_EVENT.FETCH_GRAPH_ENTITIES__SUCCESS);
// fetch project configuration
const projectConfiguration = ProjectConfiguration.serialization.fromJson(
graphBuildingMaterial[1],
);
this.editorStore.projectConfigurationEditorState.setProjectConfiguration(
projectConfiguration,
);
this.editorStore.projectConfigurationEditorState.setOriginalProjectConfiguration(
projectConfiguration,
);
// fetch project versions
yield Promise.all([
this.editorStore.sdlcState.fetchProjectVersions(),
this.editorStore.sdlcState.fetchPublishedProjectVersions(),
this.editorStore.sdlcState.fetchAuthorizedActions(),
]);
// fetch entities
const entities = graphBuildingMaterial[0];
this.editorStore.changeDetectionState.workspaceLocalLatestRevisionState.setEntities(
entities,
);
// fetch dependencies
this.editorStore.graphManagerState.dependenciesBuildState.setMessage(
`Fetching dependencies...`,
);
const dependencyEntitiesIndex =
(yield this.editorStore.graphState.getIndexedDependencyEntities()) as Map<
string,
EntitiesWithOrigin
>;
stopWatch.record(GRAPH_MANAGER_EVENT.FETCH_GRAPH_DEPENDENCIES__SUCCESS);
return {
entities,
dependencyEntitiesIndex,
};
}
/**
* Initialize the viewer store given GAV coordinate of a project.
* This flow is different than the SDLC flow as we need to fetch the project
* from Depot server here, where SDLC objects like project configurations
* are not available.
*/
private *initializeWithGAV(
groupId: string,
artifactId: string,
versionId: string,
): GeneratorFn<ProjectViewerGraphBuilderMaterial> {
const stopWatch = new StopWatch();
// fetch project data
this.editorStore.initState.setMessage(`Fetching project data...`);
const project = StoreProjectData.serialization.fromJson(
(yield flowResult(
this.editorStore.depotServerClient.getProject(groupId, artifactId),
)) as PlainObject<StoreProjectData>,
);
this.editorStore.initState.setMessage(undefined);
// fetch entities
stopWatch.record();
this.editorStore.initState.setMessage(`Fetching entities...`);
const entities = (yield this.editorStore.depotServerClient.getEntities(
project,
versionId,
)) as Entity[];
this.editorStore.initState.setMessage(undefined);
stopWatch.record(GRAPH_MANAGER_EVENT.FETCH_GRAPH_ENTITIES__SUCCESS);
// fetch dependencies
this.editorStore.graphManagerState.dependenciesBuildState.setMessage(
`Fetching dependencies...`,
);
const dependencyEntitiesIndex = (yield flowResult(
this.editorStore.depotServerClient.getIndexedDependencyEntities(
project,
versionId,
),
)) as Map<string, EntitiesWithOrigin>;
stopWatch.record(GRAPH_MANAGER_EVENT.FETCH_GRAPH_DEPENDENCIES__SUCCESS);
return {
entities,
dependencyEntitiesIndex: dependencyEntitiesIndex,
};
}
*buildGraph(
entities: Entity[],
dependencyEntitiesIndex: Map<string, EntitiesWithOrigin>,
): GeneratorFn<boolean> {
try {
const stopWatch = new StopWatch();
// initialize graph manager
yield this.editorStore.graphManagerState.graphManager.initialize(
{
env: this.editorStore.applicationStore.config.env,
tabSize: DEFAULT_TAB_SIZE,
clientConfig: {
baseUrl: this.editorStore.applicationStore.config.engineServerUrl,
queryBaseUrl:
this.editorStore.applicationStore.config.engineQueryServerUrl,
enableCompression: true,
payloadDebugger,
},
},
{
tracerService: this.editorStore.applicationStore.tracerService,
},
);
yield this.editorStore.graphManagerState.initializeSystem();
// reset
this.editorStore.graphManagerState.resetGraph();
// build dependencies
stopWatch.record();
const dependencyManager =
this.editorStore.graphManagerState.graphManager.createDependencyManager();
this.editorStore.graphManagerState.graph.dependencyManager =
dependencyManager;
const dependency_buildReport = createGraphBuilderReport();
yield this.editorStore.graphManagerState.graphManager.buildDependencies(
this.editorStore.graphManagerState.coreModel,
this.editorStore.graphManagerState.systemModel,
dependencyManager,
dependencyEntitiesIndex,
this.editorStore.graphManagerState.dependenciesBuildState,
{},
dependency_buildReport,
);
// build graph
const graph_buildReport = createGraphBuilderReport();
yield this.editorStore.graphManagerState.graphManager.buildGraph(
this.editorStore.graphManagerState.graph,
entities,
this.editorStore.graphManagerState.graphBuildState,
{
origin: this.projectGAVCoordinates
? new LegendSDLC(
this.projectGAVCoordinates.groupId,
this.projectGAVCoordinates.artifactId,
resolveVersion(this.projectGAVCoordinates.versionId),
)
: undefined,
},
graph_buildReport,
);
// report
stopWatch.record(GRAPH_MANAGER_EVENT.INITIALIZE_GRAPH__SUCCESS);
const graphBuilderReportData = {
timings:
this.editorStore.applicationStore.timeService.finalizeTimingsRecord(
stopWatch,
),
dependencies: dependency_buildReport,
dependenciesCount:
this.editorStore.graphManagerState.graph.dependencyManager
.numberOfDependencies,
graph: graph_buildReport,
};
LegendStudioTelemetryHelper.logEvent_GraphInitializationSucceeded(
this.editorStore.applicationStore.telemetryService,
graphBuilderReportData,
);
this.editorStore.applicationStore.logService.info(
LogEvent.create(GRAPH_MANAGER_EVENT.INITIALIZE_GRAPH__SUCCESS),
graphBuilderReportData,
);
// fetch available editor configurations
yield Promise.all([
this.editorStore.graphState.graphGenerationState.globalFileGenerationState.fetchAvailableFileGenerationDescriptions(),
this.editorStore.graphState.graphGenerationState.externalFormatState.fetchExternalFormatDescriptions(),
this.editorStore.graphState.graphGenerationState.externalFormatState.fetchExternalFormatDescriptions(),
this.editorStore.graphState.fetchAvailableFunctionActivatorConfigurations(),
]);
return true;
} catch (error) {
assertErrorThrown(error);
// if graph builder fails, we fall back to text-mode
this.editorStore.applicationStore.logService.error(
LogEvent.create(GRAPH_MANAGER_EVENT.GRAPH_BUILDER_FAILURE),
error,
);
if (error instanceof DependencyGraphBuilderError) {
// no recovery if dependency models cannot be built, this makes assumption that all dependencies models are compiled successfully
// TODO: we might want to handle this more gracefully when we can show people the dependency model element in the future
this.editorStore.applicationStore.notificationService.notifyError(
`Can't initialize dependency models. Error: ${error.message}`,
);
this.editorStore.applicationStore.alertService.setBlockingAlert({
message: `Can't initialize dependencies`,
prompt: 'Please use editor to better invesigate the issue',
});
} else if (error instanceof GraphDataDeserializationError) {
// if something goes wrong with de-serialization, we can't really do anything but to alert
this.editorStore.applicationStore.notificationService.notifyError(
`Can't deserialize graph. Error: ${error.message}`,
);
this.editorStore.applicationStore.alertService.setBlockingAlert({
message: `Can't deserialize graph`,
prompt: 'Please use editor to better invesigate the issue',
});
} else if (error instanceof GraphBuilderError) {
// TODO: we should split this into 2 notifications when we support multiple notifications
this.editorStore.applicationStore.notificationService.notifyError(
`Can't build graph. Redirected to text mode for debugging. Error: ${error.message}`,
);
this.editorStore.applicationStore.logService.error(
LogEvent.create(GRAPH_MANAGER_EVENT.GRAPH_BUILDER_FAILURE),
error,
);
yield flowResult(
this.editorStore.switchModes(GRAPH_EDITOR_MODE.GRAMMAR_TEXT, {
isGraphBuildFailure: true,
}),
);
if (this.editorStore.graphEditorMode.mode === GRAPH_EDITOR_MODE.FORM) {
// nothing we can do here so we will just block the user
this.editorStore.applicationStore.alertService.setBlockingAlert({
message: `Can't compose Pure code from graph models`,
prompt: 'Please use editor to better invesigate the issue',
});
return false;
}
} else {
this.editorStore.applicationStore.notificationService.notifyError(
error,
);
}
return false;
}
}
*initialize(params: ProjectViewerPathParams): GeneratorFn<void> {
if (!this.editorStore.initState.isInInitialState) {
return;
}
const { gav, projectId } = params;
this.editorStore.initState.inProgress();
const onLeave = (hasBuildSucceeded: boolean): void => {
this.editorStore.initState.complete(hasBuildSucceeded);
};
try {
let graphBuilderMaterial: ProjectViewerGraphBuilderMaterial;
if (projectId) {
graphBuilderMaterial = (yield flowResult(
this.initializeWithProjectInformation(
projectId,
params.versionId,
params.revisionId,
),
)) as ProjectViewerGraphBuilderMaterial;
} else if (gav) {
this.projectGAVCoordinates = parseGAVCoordinates(gav);
const { groupId, artifactId, versionId } = this.projectGAVCoordinates;
graphBuilderMaterial = (yield flowResult(
this.initializeWithGAV(groupId, artifactId, versionId),
)) as ProjectViewerGraphBuilderMaterial;
} else {
throw new IllegalStateError(
`Can't initialize viewer when neither 'projectId' nor 'gav' is provided`,
);
}
const graphBuilderResult = (yield flowResult(
this.buildGraph(
graphBuilderMaterial.entities,
graphBuilderMaterial.dependencyEntitiesIndex,
),
)) as boolean;
if (!graphBuilderResult) {
onLeave(false);
return;
}
// generate
// NOTE: if we fetch the entities from a published project
// there is no need to generate since the generated elements are already included
if (!gav) {
this.editorStore.initState.setMessage(`Generating elements...`);
if (
this.editorStore.graphManagerState.graph.ownGenerationSpecifications
.length
) {
yield flowResult(
this.editorStore.graphState.graphGenerationState.globalGenerate(),
);
}
this.editorStore.initState.setMessage(undefined);
}
// build explorer tree
this.editorStore.explorerTreeState.buildImmutableModelTrees();
this.editorStore.explorerTreeState.build();
// open element if provided an element path
if (
this.editorStore.graphManagerState.graphBuildState.hasSucceeded &&
this.editorStore.explorerTreeState.buildState.hasCompleted &&
this.initialEntityPath
) {
try {
this.editorStore.graphEditorMode.openElement(
this.editorStore.graphManagerState.graph.getElement(
this.initialEntityPath,
),
);
} catch {
const elementPath = this.initialEntityPath;
this.initialEntityPath = undefined;
throw new AssertionError(
`Can't find element with path '${elementPath}'`,
);
}
}
// initialize workflow manager
// NOTE: We will not show workflow viewer when `GAV` coordinates are provided
// as we don't know which sdlc instance to fetch from.
// Revision will be supported once `SDLC` adds the workflow apis.
if (this.version) {
this.workflowManagerState = new ProjectVersionWorkflowManagerState(
this.editorStore,
this.editorStore.sdlcState,
this.version,
);
} else if (!this.projectGAVCoordinates && !this.revision) {
this.workflowManagerState = new ProjectWorkflowManagerState(
this.editorStore,
this.editorStore.sdlcState,
);
}
onLeave(true);
} catch (error) {
assertErrorThrown(error);
this.editorStore.applicationStore.logService.error(
LogEvent.create(LEGEND_STUDIO_APP_EVENT.SDLC_MANAGER_FAILURE),
error,
);
this.editorStore.applicationStore.notificationService.notifyError(error);
onLeave(false);
}
}
}