UNPKG

@finos/legend-application-studio

Version:
566 lines 25.4 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 { 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