@finos/legend-application-studio
Version:
Legend Studio application core
566 lines • 25.4 kB
JavaScript
/**
* 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 { flow, observable, makeObservable, flowResult, action, computed, } from 'mobx';
import { ActionState, assertErrorThrown, LogEvent, isNonNullable, guaranteeNonNullable, uuid, } from '@finos/legend-shared';
import { EngineError } from '@finos/legend-graph';
import { ProjectDependencyCoordinates, buildConflictsPaths, buildDependencyReport, RawProjectDependencyReport, } from '@finos/legend-server-depot';
import { LEGEND_STUDIO_APP_EVENT } from '../../../../__lib__/LegendStudioEvent.js';
import { ProjectDependencyExclusion, } from '@finos/legend-server-sdlc';
import { generateGAVCoordinates, } from '@finos/legend-storage';
export class ProjectDependencyConflictTreeNodeData {
id;
label;
childrenIds;
isSelected;
isOpen;
constructor(id) {
this.id = id;
this.label = id;
}
}
export class ConflictTreeNodeData extends ProjectDependencyConflictTreeNodeData {
conflict;
constructor(conflict) {
super(`${conflict.groupId}:${conflict.artifactId}`);
this.conflict = conflict;
}
get description() {
return this.id;
}
}
export class ConflictVersionNodeData extends ProjectDependencyConflictTreeNodeData {
versionConflict;
constructor(conflict) {
super(`${conflict.version.groupId}:${conflict.version.artifactId}.${conflict.version.versionId}`);
this.versionConflict = conflict;
this.label = this.versionConflict.version.versionId;
}
get description() {
return this.id;
}
}
export class ProjectDependencyTreeNodeData extends ProjectDependencyConflictTreeNodeData {
value;
constructor(id, value) {
super(id);
this.value = value;
this.label = value.artifactId;
}
get description() {
return `${this.value.groupId}:${this.value.artifactId}:${this.value.versionId}`;
}
}
export const buildDependencyNodeChildren = (parentNode, treeNodes) => {
if (!parentNode.childrenIds) {
const value = parentNode.value;
const childrenNodes = value.dependencies.map((projectVersion) => {
const childId = `${parentNode.id}.${projectVersion.id}`;
const childNode = new ProjectDependencyTreeNodeData(childId, projectVersion);
treeNodes.set(childId, childNode);
return childNode;
});
parentNode.childrenIds = childrenNodes.map((c) => c.id);
}
};
const findRootNode = (versionNode, treeData) => {
if (!treeData.rootIds.includes(versionNode.id)) {
return undefined;
}
return Array.from(treeData.nodes.values()).find((node) => node.id === versionNode.id && node.value === versionNode);
};
const walkNode = (node, visited, treeData) => {
if (!visited.has(node.value)) {
node.isOpen = true;
buildDependencyNodeChildren(node, treeData.nodes);
visited.add(node.value);
node.childrenIds
?.map((nodeId) => treeData.nodes.get(nodeId))
.filter(isNonNullable)
.forEach((n) => walkNode(n, visited, treeData));
}
else {
buildDependencyNodeChildren(node, treeData.nodes);
}
};
export const openAllDependencyNodesInTree = (treeData, graph) => {
const visited = new Set();
graph.rootNodes
.map((node) => findRootNode(node, treeData))
.filter(isNonNullable)
.forEach((node) => walkNode(node, visited, treeData));
};
const buildDependencyTreeData = (report) => {
const nodes = new Map();
const rootNodes = report.graph.rootNodes.map((versionNode) => {
const node = new ProjectDependencyTreeNodeData(versionNode.id, versionNode);
nodes.set(node.id, node);
buildDependencyNodeChildren(node, nodes);
return node;
});
const rootIds = rootNodes.map((node) => node.id);
return { rootIds, nodes };
};
const buildFlattenDependencyTreeData = (report) => {
const nodes = new Map();
const rootIds = [];
Array.from(report.graph.nodes.entries()).forEach(([key, value]) => {
const id = value.id;
const node = new ProjectDependencyTreeNodeData(id, value);
nodes.set(id, node);
rootIds.push(id);
buildDependencyNodeChildren(node, nodes);
});
return { rootIds, nodes };
};
export var DEPENDENCY_REPORT_TAB;
(function (DEPENDENCY_REPORT_TAB) {
DEPENDENCY_REPORT_TAB["EXPLORER"] = "EXPLORER";
DEPENDENCY_REPORT_TAB["CONFLICTS"] = "CONFLICTS";
DEPENDENCY_REPORT_TAB["RESOLUTION"] = "RESOLUTION";
})(DEPENDENCY_REPORT_TAB || (DEPENDENCY_REPORT_TAB = {}));
const buildTreeDataFromConflictVersion = (conflictVersionNode, nodes) => conflictVersionNode.versionConflict.pathsToVersion
.map((path, idx) => {
if (!path.length) {
return undefined;
}
const pathIterator = path.values();
let rootNode;
let parentNode;
let currentVersion;
while ((currentVersion = pathIterator.next().value)) {
const id = parentNode
? `${parentNode.id}.${currentVersion.id}`
: `path${idx}_${currentVersion.id}`;
const node = new ProjectDependencyTreeNodeData(id, currentVersion);
node.childrenIds = [];
nodes.set(id, node);
if (parentNode) {
parentNode.childrenIds = [node.id];
}
else {
rootNode = node;
}
parentNode = node;
}
return rootNode;
})
.filter(isNonNullable);
const buildTreeDataFromConflict = (conflict, paths) => {
const rootNode = new ConflictTreeNodeData(conflict);
const rootIds = [rootNode.id];
const nodes = new Map();
nodes.set(rootNode.id, rootNode);
const versionConflictNodes = paths.map((versionConflict) => {
const projectVersionNode = new ConflictVersionNodeData(versionConflict);
nodes.set(projectVersionNode.id, projectVersionNode);
const pathNodes = buildTreeDataFromConflictVersion(projectVersionNode, nodes);
projectVersionNode.childrenIds = pathNodes.map((n) => n.id);
return projectVersionNode;
});
rootNode.childrenIds = versionConflictNodes.map((n) => n.id);
return { rootIds, nodes };
};
export class ProjectDependencyConflictState {
uuid = uuid();
report;
conflict;
paths;
treeData;
constructor(report, conflict, paths) {
makeObservable(this, {
treeData: observable.ref,
setTreeData: action,
});
this.report = report;
this.conflict = conflict;
this.paths = paths;
this.treeData = buildTreeDataFromConflict(conflict, paths);
}
setTreeData(treeData) {
this.treeData = treeData;
}
get versionNodes() {
return this.paths.map((e) => e.version);
}
}
export class ProjectDependencyEditorState {
configState;
editorStore;
isReadOnly;
reportTab;
fetchingDependencyInfoState = ActionState.create();
dependencyReport;
dependencyTreeData;
flattenDependencyTreeData;
conflictStates;
expandConflictsState = ActionState.create();
buildConflictPathState = ActionState.create();
validatingDependenciesState = ActionState.create();
resolvingCompatibleDependenciesState = ActionState.create();
resolutionResult;
// Exclusions management
selectedDependencyForExclusions;
constructor(configState, editorStore) {
makeObservable(this, {
dependencyReport: observable,
fetchingDependencyInfoState: observable,
dependencyTreeData: observable.ref,
flattenDependencyTreeData: observable.ref,
conflictStates: observable,
reportTab: observable,
expandConflictsState: observable,
buildConflictPathState: observable,
validatingDependenciesState: observable,
resolvingCompatibleDependenciesState: observable,
resolutionResult: observable,
selectedDependencyForExclusions: observable,
hasAnyExclusions: computed,
hasDependencyChanges: computed,
setReportTab: action,
expandAllConflicts: action,
setFlattenDependencyTreeData: action,
clearTrees: action,
setTreeData: action,
setDependencyTreeData: action,
buildConflictPaths: action,
setConflictStates: action,
setSelectedDependencyForExclusions: action,
addExclusion: action,
addExclusionByCoordinate: action,
removeExclusion: action,
removeExclusionByCoordinate: action,
clearExclusions: action,
getExclusions: action,
getExclusionCoordinates: action,
clearResolutionResult: action,
applyResolvedDependencies: flow,
fetchDependencyReport: flow,
validateAndFetchDependencyReport: flow,
resolveCompatibleDependencies: flow,
});
this.configState = configState;
this.editorStore = editorStore;
this.isReadOnly = editorStore.isInViewerMode;
}
expandAllConflicts() {
if (this.conflictStates) {
this.expandConflictsState.inProgress();
this.conflictStates.forEach((c) => {
const treeData = c.treeData;
Array.from(treeData.nodes.values()).forEach((n) => (n.isOpen = true));
});
this.conflictStates.forEach((c) => {
c.setTreeData({ ...c.treeData });
});
this.expandConflictsState.complete();
}
}
setTreeData(treeData, flattenView) {
if (flattenView) {
this.setFlattenDependencyTreeData(treeData);
}
else {
this.setDependencyTreeData(treeData);
}
}
setReportTab(tab) {
this.reportTab = tab;
}
setDependencyTreeData(tree) {
this.dependencyTreeData = tree;
}
setConflictStates(val) {
this.conflictStates = val;
}
setFlattenDependencyTreeData(tree) {
this.flattenDependencyTreeData = tree;
}
get projectConfiguration() {
return this.configState.projectConfiguration;
}
setSelectedDependencyForExclusions(dependency) {
this.selectedDependencyForExclusions = dependency;
}
findProjectDependency(dependencyId) {
return this.projectConfiguration?.projectDependencies.find((dep) => dep.projectId === dependencyId);
}
addExclusion(dependencyId, exclusion) {
const projectDependency = this.findProjectDependency(dependencyId);
if (!projectDependency) {
return;
}
const existingExclusion = this.findExistingExclusion(dependencyId, generateGAVCoordinates(guaranteeNonNullable(exclusion.groupId), guaranteeNonNullable(exclusion.artifactId), undefined));
if (!existingExclusion) {
const currentExclusions = projectDependency.exclusions ?? [];
projectDependency.setExclusions([...currentExclusions, exclusion]);
}
}
addExclusionByCoordinate(dependencyId, exclusionCoordinate) {
const exclusion = ProjectDependencyExclusion.fromCoordinate(exclusionCoordinate);
this.addExclusion(dependencyId, exclusion);
}
removeExclusion(dependencyId, exclusion) {
const projectDependency = this.findProjectDependency(dependencyId);
if (!projectDependency?.exclusions) {
return;
}
const coordinate = generateGAVCoordinates(guaranteeNonNullable(exclusion.groupId), guaranteeNonNullable(exclusion.artifactId), undefined);
const index = this.findExclusionIndex(dependencyId, coordinate);
if (index > -1) {
const updatedExclusions = [...projectDependency.exclusions];
updatedExclusions.splice(index, 1);
projectDependency.setExclusions(updatedExclusions);
}
}
removeExclusionByCoordinate(dependencyId, exclusionCoordinate) {
const projectDependency = this.findProjectDependency(dependencyId);
if (!projectDependency?.exclusions) {
return;
}
const index = this.findExclusionIndex(dependencyId, exclusionCoordinate);
if (index > -1) {
const updatedExclusions = [...projectDependency.exclusions];
updatedExclusions.splice(index, 1);
projectDependency.setExclusions(updatedExclusions);
}
}
clearExclusions(dependencyId) {
if (dependencyId) {
const projectDependency = this.findProjectDependency(dependencyId);
if (projectDependency) {
projectDependency.setExclusions([]);
}
}
else {
this.projectConfiguration?.projectDependencies.forEach((dep) => {
dep.setExclusions([]);
});
}
}
getExclusions(dependencyId) {
const projectDependency = this.findProjectDependency(dependencyId);
return projectDependency?.exclusions ?? [];
}
get hasAnyExclusions() {
return (this.projectConfiguration?.projectDependencies.some((dep) => dep.exclusions && dep.exclusions.length > 0) ?? false);
}
get hasDependencyChanges() {
if (!this.configState.originalProjectConfiguration) {
return false;
}
const originalDeps = this.configState.originalProjectConfiguration.projectDependencies;
const currentDeps = this.configState.currentProjectConfiguration.projectDependencies;
return (currentDeps.some((currentDep) => !originalDeps.find((origDep) => origDep.hashCode === currentDep.hashCode)) ||
originalDeps.some((origDep) => !currentDeps.find((currentDep) => currentDep.hashCode === origDep.hashCode)));
}
getExclusionCoordinates(dependencyId) {
const exclusions = this.getExclusions(dependencyId);
return exclusions.map((e) => generateGAVCoordinates(guaranteeNonNullable(e.groupId), guaranteeNonNullable(e.artifactId), undefined));
}
findExistingExclusion(dependencyId, coordinate) {
const projectDependency = this.findProjectDependency(dependencyId);
if (!projectDependency?.exclusions) {
return undefined;
}
for (let i = 0; i < projectDependency.exclusions.length; i++) {
const exclusion = guaranteeNonNullable(projectDependency.exclusions[i]);
const exclusionCoordinate = generateGAVCoordinates(guaranteeNonNullable(exclusion.groupId), guaranteeNonNullable(exclusion.artifactId), undefined);
if (exclusionCoordinate === coordinate) {
return projectDependency.exclusions[i];
}
}
return undefined;
}
findExclusionIndex(dependencyId, coordinate) {
const projectDependency = this.findProjectDependency(dependencyId);
if (!projectDependency?.exclusions) {
return -1;
}
for (let i = 0; i < projectDependency.exclusions.length; i++) {
const exclusion = guaranteeNonNullable(projectDependency.exclusions[i]);
const exclusionCoordinate = generateGAVCoordinates(guaranteeNonNullable(exclusion.groupId), guaranteeNonNullable(exclusion.artifactId), undefined);
if (exclusionCoordinate === coordinate) {
return i;
}
}
return -1;
}
*fetchDependencyReport() {
try {
this.fetchingDependencyInfoState.inProgress();
this.dependencyReport = undefined;
this.clearTrees();
this.setConflictStates(undefined);
if (this.projectConfiguration?.projectDependencies) {
const dependencyCoordinates = (yield flowResult(this.editorStore.graphState.buildProjectDependencyCoordinates(this.projectConfiguration.projectDependencies)));
const dependencyInfoRaw = (yield this.editorStore.depotServerClient.analyzeDependencyTree(dependencyCoordinates.map((e) => ProjectDependencyCoordinates.serialization.toJson(e))));
const rawdependencyReport = RawProjectDependencyReport.serialization.fromJson(dependencyInfoRaw);
const report = buildDependencyReport(rawdependencyReport);
this.dependencyReport = report;
this.setDependencyTreeData(buildDependencyTreeData(report));
this.setFlattenDependencyTreeData(buildFlattenDependencyTreeData(report));
}
this.fetchingDependencyInfoState.complete();
}
catch (error) {
assertErrorThrown(error);
this.fetchingDependencyInfoState.fail();
this.dependencyReport = undefined;
this.editorStore.applicationStore.logService.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.DEPOT_MANAGER_FAILURE), error);
}
}
*validateAndFetchDependencyReport() {
try {
this.validatingDependenciesState.inProgress();
yield flowResult(this.fetchDependencyReport());
try {
const dependencyEntitiesIndex = (yield this.editorStore.graphState.getIndexedDependencyEntities(this.dependencyReport));
const dependencyEntities = Array.from(dependencyEntitiesIndex.values()).flatMap((entitiesWithOrigin) => entitiesWithOrigin.entities);
const projectElements = this.editorStore.graphManagerState.graph.allOwnElements;
const projectEntities = projectElements.map((element) => this.editorStore.graphManagerState.graphManager.elementToEntity(element));
const allEntities = [...dependencyEntities, ...projectEntities];
yield this.editorStore.graphManagerState.graphManager.compileEntities(allEntities);
this.validatingDependenciesState.complete();
this.editorStore.applicationStore.notificationService.notifySuccess('Dependencies validated successfully - no compilation errors');
}
catch (error) {
assertErrorThrown(error);
this.validatingDependenciesState.fail();
if (error instanceof EngineError) {
const errorLines = error.message
.split('\n')
.filter((line) => line.trim().length > 0);
const errorPreview = errorLines.slice(0, 5).join('; ');
const remainingCount = Math.max(0, errorLines.length - 5);
const errorMessage = remainingCount > 0
? `Dependencies cause compilation errors: ${errorPreview} and ${remainingCount} more`
: `Dependencies cause compilation errors: ${errorPreview}`;
this.editorStore.applicationStore.notificationService.notifyError(errorMessage);
}
else {
this.editorStore.applicationStore.notificationService.notifyError(`Failed to validate dependencies: ${error.message}`);
}
}
}
catch (error) {
assertErrorThrown(error);
this.validatingDependenciesState.fail();
this.editorStore.applicationStore.notificationService.notifyError(`Failed to validate dependencies: ${error.message}`);
}
}
buildConflictPaths() {
const report = this.dependencyReport;
if (report) {
this.setConflictStates(undefined);
this.buildConflictPathState.inProgress();
try {
report.conflictInfo = buildConflictsPaths(report);
const conflictStates = Array.from(report.conflictInfo.entries()).map(([conflict, paths]) => new ProjectDependencyConflictState(report, conflict, paths));
this.setConflictStates(conflictStates);
this.buildConflictPathState.complete();
}
catch (error) {
assertErrorThrown(error);
this.setConflictStates([]);
this.buildConflictPathState.fail();
this.editorStore.applicationStore.notificationService.notifyError(`Unable to build conflict paths ${error.message}`);
}
}
}
clearTrees() {
this.flattenDependencyTreeData = undefined;
this.dependencyTreeData = undefined;
this.selectedDependencyForExclusions = undefined;
}
clearResolutionResult() {
this.resolutionResult = undefined;
}
*applyResolvedDependencies() {
if (this.resolutionResult &&
this.resolutionResult.success &&
this.projectConfiguration?.projectDependencies) {
const resolvedDeps = this.resolutionResult.resolvedVersions;
// Update the configuration
this.projectConfiguration.projectDependencies.forEach((dep) => {
const resolved = resolvedDeps.find((r) => r.groupId === dep.groupId && r.artifactId === dep.artifactId);
if (resolved) {
dep.setVersionId(resolved.versionId);
}
});
const configState = this.configState;
for (const dep of this.projectConfiguration.projectDependencies) {
if (!configState.versions.has(dep.projectId)) {
try {
const _versions = (yield this.editorStore.depotServerClient.getVersions(guaranteeNonNullable(dep.groupId), guaranteeNonNullable(dep.artifactId), true));
configState.versions.set(dep.projectId, _versions);
}
catch (error) {
assertErrorThrown(error);
this.editorStore.applicationStore.logService.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.DEPOT_MANAGER_FAILURE), `Failed to fetch versions for ${dep.projectId}: ${error.message}`);
}
}
}
// Refresh the dependency report
try {
yield flowResult(this.fetchDependencyReport());
this.editorStore.applicationStore.notificationService.notifySuccess(`Successfully applied ${resolvedDeps.length} resolved dependencies`);
this.clearResolutionResult();
}
catch (error) {
assertErrorThrown(error);
this.editorStore.applicationStore.notificationService.notifyError(`Failed to refresh dependency report: ${error.message}`);
}
}
}
*resolveCompatibleDependencies(backtrackVersions) {
this.resolvingCompatibleDependenciesState.inProgress();
try {
if (this.projectConfiguration?.projectDependencies) {
const dependencyCoordinates = (yield flowResult(this.editorStore.graphState.buildProjectDependencyCoordinates(this.projectConfiguration.projectDependencies)));
const rawResponse = (yield this.editorStore.depotServerClient.resolveCompatibleDependencies(dependencyCoordinates.map((e) => ProjectDependencyCoordinates.serialization.toJson(e)), backtrackVersions));
const resolvedVersions = rawResponse.resolvedVersions.map((coord) => ProjectDependencyCoordinates.serialization.fromJson(coord));
const conflicts = rawResponse.conflicts.map((conflict) => {
const result = {
groupId: conflict.groupId,
artifactId: conflict.artifactId,
conflictingVersions: conflict.conflictingVersions,
};
if (conflict.suggestedOverride) {
result.suggestedOverride =
ProjectDependencyCoordinates.serialization.fromJson(conflict.suggestedOverride);
}
return result;
});
const suggestedOverrides = rawResponse.suggestedOverrides?.map((coord) => ProjectDependencyCoordinates.serialization.fromJson(coord));
this.resolutionResult = {
success: rawResponse.success,
resolvedVersions,
conflicts,
failureReason: rawResponse.failureReason ?? null,
...(suggestedOverrides ? { suggestedOverrides } : {}),
};
this.setReportTab(DEPENDENCY_REPORT_TAB.RESOLUTION);
}
this.resolvingCompatibleDependenciesState.complete();
}
catch (error) {
assertErrorThrown(error);
this.resolvingCompatibleDependenciesState.fail();
this.editorStore.applicationStore.notificationService.notifyError(`Failed to resolve dependencies: ${error.message}`);
}
}
}
//# sourceMappingURL=ProjectDependencyEditorState.js.map