/** * 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 { useApplicationStore, DEFAULT_TAB_SIZE, } from '@finos/legend-application'; import { type SelectComponent, type TreeData, type TreeNodeContainerProps, compareLabelFn, clsx, CustomSelectorInput, TimesIcon, Dialog, ControlledDropdownMenu, CaretDownIcon, MenuContentItem, MenuContent, Modal, ModalBody, ModalFooter, ModalHeader, PanelLoadingIndicator, TreeView, ChevronDownIcon, ChevronRightIcon, ContextMenu, CompressIcon, SubjectIcon, ViewHeadlineIcon, ExpandAllIcon, BlankPanelContent, VersionsIcon, RepoIcon, ModalFooterButton, Button, Panel, PanelHeader, PanelHeaderActions, PanelHeaderActionItem, PanelContentLists, CircleNotchIcon, } from '@finos/legend-art'; import { MASTER_SNAPSHOT_ALIAS, type ProjectDependencyGraphReport, type StoreProjectData, SNAPSHOT_VERSION_ALIAS, type ConflictingVersionInfo, type ProjectDependencyCoordinates, } from '@finos/legend-server-depot'; import { type ProjectDependency, type ProjectDependencyExclusion, } from '@finos/legend-server-sdlc'; import { ActionState, assertErrorThrown, guaranteeNonNullable, isNonNullable, LogEvent, prettyCONSTName, compareSemVerVersions, } from '@finos/legend-shared'; import { generateGAVCoordinates } from '@finos/legend-storage'; import { flowResult } from 'mobx'; import { observer } from 'mobx-react-lite'; import { forwardRef, useCallback, useEffect, useRef, useState } from 'react'; import { ProjectConfigurationEditorState } from '../../../../stores/editor/editor-state/project-configuration-editor-state/ProjectConfigurationEditorState.js'; import { type ProjectDependencyConflictTreeNodeData, ConflictTreeNodeData, ConflictVersionNodeData, buildDependencyNodeChildren, DEPENDENCY_REPORT_TAB, openAllDependencyNodesInTree, ProjectDependencyTreeNodeData, type ProjectDependencyEditorState, } from '../../../../stores/editor/editor-state/project-configuration-editor-state/ProjectDependencyEditorState.js'; import { LEGEND_STUDIO_APP_EVENT } from '../../../../__lib__/LegendStudioEvent.js'; import { generateViewProjectByGAVRoute, generateViewProjectRoute, generateViewVersionRoute, } from '../../../../__lib__/LegendStudioNavigation.js'; import { LEGEND_STUDIO_TEST_ID } from '../../../../__lib__/LegendStudioTesting.js'; import { useEditorStore } from '../../EditorStoreProvider.js'; export interface VersionOption { label: string; value: string; } interface ProjectOption { label: string; value: StoreProjectData; } const buildProjectOption = (project: StoreProjectData): ProjectOption => ({ label: project.coordinates, value: project, }); const ProjectDependencyActions = observer( (props: { dependencyEditorState: ProjectDependencyEditorState }) => { const { dependencyEditorState: dependencyEditorState } = props; const applicationStore = useApplicationStore(); const hasConflicts = dependencyEditorState.dependencyReport?.conflicts.length; const hasDependencyChanges = dependencyEditorState.hasDependencyChanges; const viewTree = (): void => { if (dependencyEditorState.dependencyReport) { dependencyEditorState.setReportTab(DEPENDENCY_REPORT_TAB.EXPLORER); } }; const viewConflict = (): void => { if (dependencyEditorState.dependencyReport) { dependencyEditorState.setReportTab(DEPENDENCY_REPORT_TAB.CONFLICTS); } }; const validateDependencies = (): void => { flowResult( dependencyEditorState.validateAndFetchDependencyReport(), ).catch(applicationStore.alertUnhandledError); }; return (
); }, ); const formatOptionLabel = (option: ProjectOption): React.ReactNode => (
{option.value.projectId}
{option.value.coordinates}
); const DependencyTreeNodeContextMenu = observer( forwardRef< HTMLDivElement, { node: ProjectDependencyConflictTreeNodeData; } >(function DependencyTreeNodeContextMenu(props, ref) { const { node } = props; const applicationStore = useApplicationStore(); const getViewProjectUrl = (): string => { let groupId: string | undefined; let artifactId: string | undefined; let versionId: string | undefined; if (node instanceof ConflictTreeNodeData) { groupId = node.conflict.groupId; artifactId = node.conflict.artifactId; } else if (node instanceof ConflictVersionNodeData) { groupId = node.versionConflict.conflict.groupId; artifactId = node.versionConflict.conflict.artifactId; versionId = node.versionConflict.version.versionId; } else if (node instanceof ProjectDependencyTreeNodeData) { groupId = node.value.groupId; artifactId = node.value.artifactId; versionId = node.value.versionId; } return generateViewProjectByGAVRoute( guaranteeNonNullable(groupId), guaranteeNonNullable(artifactId), versionId === MASTER_SNAPSHOT_ALIAS ? SNAPSHOT_VERSION_ALIAS : versionId, ); }; const getSDLCProjectUrl = (): string | undefined => { if (node instanceof ConflictTreeNodeData) { const version = node.conflict.versions[0]; return version ? generateViewProjectRoute(version.projectId) : undefined; } else if (node instanceof ConflictVersionNodeData) { return generateViewVersionRoute( node.versionConflict.version.projectId, node.versionConflict.version.artifactId, ); } else if (node instanceof ProjectDependencyTreeNodeData) { return generateViewVersionRoute( node.value.projectId, node.value.versionId, ); } return undefined; }; const sdlcProjectUrl = getSDLCProjectUrl(); const viewProjectUrl = getViewProjectUrl(); const viewProject = (): void => { applicationStore.navigationService.navigator.visitAddress( applicationStore.navigationService.navigator.generateAddress( viewProjectUrl, ), ); }; const viewSDLCProject = (): void => { if (sdlcProjectUrl) { applicationStore.navigationService.navigator.visitAddress( applicationStore.navigationService.navigator.generateAddress( sdlcProjectUrl, ), ); } }; return ( Visit Project Visit SDLC Project ); }), ); const DependencyTreeNodeContainer: React.FC< TreeNodeContainerProps< ProjectDependencyTreeNodeData, { onNodeExpand: (node: ProjectDependencyTreeNodeData) => void; } > > = (props) => { const { node, level, stepPaddingInRem, onNodeSelect } = props; const isExpandable = Boolean(node.childrenIds?.length); const selectNode = (): void => onNodeSelect?.(node); const value = node.value; const nodeExpandIcon = isExpandable ? ( node.isOpen ? ( ) : ( ) ) : (
); return ( } menuProps={{ elevation: 7 }} >
{nodeExpandIcon}
); }; const DependencyTreeView: React.FC<{ treeData: TreeData; setTreeData: (treeData: TreeData) => void; }> = (props) => { const { treeData, setTreeData } = props; const onNodeExpand = (node: ProjectDependencyTreeNodeData): void => { if (node.childrenIds?.length) { node.isOpen = !node.isOpen; node.childrenIds .map((id) => treeData.nodes.get(id)) .filter(isNonNullable) .forEach((c) => buildDependencyNodeChildren(c, treeData.nodes)); } setTreeData({ ...treeData }); }; const onNodeSelect = (node: ProjectDependencyTreeNodeData): void => { buildDependencyNodeChildren(node, treeData.nodes); onNodeExpand(node); setTreeData({ ...treeData }); }; const getChildNodes = ( node: ProjectDependencyTreeNodeData, ): ProjectDependencyTreeNodeData[] => { if (!node.childrenIds || node.childrenIds.length === 0) { return []; } const childrenNodes = node.childrenIds .map((id) => treeData.nodes.get(id)) .filter(isNonNullable); return childrenNodes; }; return ( ); }; const ConflictTreeNodeContainer: React.FC< TreeNodeContainerProps< ProjectDependencyConflictTreeNodeData, { onNodeExpand: (node: ProjectDependencyConflictTreeNodeData) => void; } > > = (props) => { const { node, level, stepPaddingInRem, onNodeSelect } = props; const isExpandable = Boolean(node.childrenIds?.length); const selectNode = (): void => onNodeSelect?.(node); const nodeExpandIcon = isExpandable ? ( node.isOpen ? ( ) : ( ) ) : (
); return ( } menuProps={{ elevation: 7 }} >
{nodeExpandIcon}
{node instanceof ConflictTreeNodeData && (
)} {node instanceof ConflictVersionNodeData && (
)}
{node instanceof ProjectDependencyTreeNodeData && (
)}
); }; const ConflictDependencyTreeView: React.FC<{ treeData: TreeData; setTreeData: ( treeData: TreeData, ) => void; }> = (props) => { const { treeData, setTreeData } = props; const onNodeExpand = (node: ProjectDependencyConflictTreeNodeData): void => { if (node.childrenIds?.length) { node.isOpen = !node.isOpen; } setTreeData({ ...treeData }); }; const onNodeSelect = (node: ProjectDependencyConflictTreeNodeData): void => { onNodeExpand(node); setTreeData({ ...treeData }); }; const getChildNodes = ( node: ProjectDependencyConflictTreeNodeData, ): ProjectDependencyConflictTreeNodeData[] => { if (!node.childrenIds || node.childrenIds.length === 0) { return []; } const childrenNodes = node.childrenIds .map((id) => treeData.nodes.get(id)) .filter(isNonNullable); return childrenNodes; }; return ( ); }; const collapseTreeData = ( treeData: TreeData, ): void => { Array.from(treeData.nodes.values()).forEach((node) => { node.isOpen = false; }); }; export const getConflictsString = ( report: ProjectDependencyGraphReport, ): string => Array.from(report.conflictInfo.entries()) .map(([k, conflictVersionPaths]) => { const base = `project:\n${ ' '.repeat(DEFAULT_TAB_SIZE) + generateGAVCoordinates(k.groupId, k.artifactId, undefined) }`; const versionConflictString = conflictVersionPaths .map((conflictVersion) => { const versions = `version: ${conflictVersion.version.versionId}\n`; const paths = `paths:\n${conflictVersion.pathsToVersion .map( (p, idx) => `${' '.repeat(DEFAULT_TAB_SIZE) + (idx + 1)}:\n${p .map((l) => l.id) .join('>')}`, ) .join('\n')}`; return versions + paths; }) .join('\n'); return `${base}\n${versionConflictString}`; }) .join('\n\n'); const ProjectDependencyConflictViewer = observer( (props: { dependencyEditorState: ProjectDependencyEditorState; report: ProjectDependencyGraphReport; }) => { const { report, dependencyEditorState } = props; const hasConflict = Boolean(report.conflicts.length); const collapseTree = (): void => { dependencyEditorState.conflictStates?.forEach((c) => { const treeData = c.treeData; Array.from(treeData.nodes.values()).forEach((n) => (n.isOpen = false)); c.setTreeData({ ...treeData }); }); }; const expandAllNodes = (): void => { dependencyEditorState.expandAllConflicts(); }; useEffect(() => { if (hasConflict && !dependencyEditorState.conflictStates) { dependencyEditorState.buildConflictPaths(); } }, [dependencyEditorState, hasConflict]); return (
{hasConflict && dependencyEditorState.conflictStates && (
{dependencyEditorState.conflictStates.map((c) => ( , ) => c.setTreeData(treeData)} /> ))}
)} {!hasConflict && No Conflicts}
); }, ); const ProjectDependencyResolutionViewer = observer( (props: { dependencyEditorState: ProjectDependencyEditorState }) => { const { dependencyEditorState } = props; const applicationStore = dependencyEditorState.editorStore.applicationStore; const [versionBacktrackCount, setVersionBacktrackCount] = useState(5); const resolutionResult = dependencyEditorState.resolutionResult; const resolveConflicts = (): void => { dependencyEditorState.clearResolutionResult(); flowResult( dependencyEditorState.resolveCompatibleDependencies( versionBacktrackCount, ), ).catch(applicationStore.alertUnhandledError); }; const applyResolution = (): void => { flowResult(dependencyEditorState.applyResolvedDependencies()).catch( applicationStore.alertUnhandledError, ); }; const cancelResolution = (): void => { dependencyEditorState.clearResolutionResult(); }; return ( {!resolutionResult?.success && ( <>
setVersionBacktrackCount( Math.max(1, parseInt(e.target.value || '1', 10)), ) } title="Number of previous versions to check backward from the latest version when searching for compatible dependency versions" className="input input--dark project-dependency-editor__backtrack-input" placeholder="Backtrack versions" />
versions to check
)}
{!resolutionResult && ( Click "Resolve Compatible Dependencies" to find compatible versions for conflicting dependencies )} {resolutionResult && resolutionResult.success && (
✓ Compatible Versions Found
Found compatible versions by checking {versionBacktrackCount}{' '} versions back. Review and confirm to apply.
{resolutionResult.resolvedVersions.map((dep) => (
{dep.groupId}:{dep.artifactId}
→ {dep.versionId}
))}
)} {resolutionResult && !resolutionResult.success && (
✗ Unable to Resolve Conflicts
{resolutionResult.failureReason ?? 'No compatible versions found'}{' '} within {versionBacktrackCount} versions.
💡 Tip: Try increasing the backtrack version count or manually adjust your dependency versions.
Conflicting Dependencies ({resolutionResult.conflicts.length})
{resolutionResult.conflicts.map((conflict) => { const suggestedVersionId = conflict.suggestedOverride?.versionId; return (
{conflict.groupId}:{conflict.artifactId}
{suggestedVersionId && (
💡 Suggested Override: {suggestedVersionId}
)}
{conflict.conflictingVersions.map( (versionInfo: ConflictingVersionInfo) => { const isSuggestedVersion = versionInfo.version === suggestedVersionId; return (
{versionInfo.version} {isSuggestedVersion && ( {' '} [suggested override] )} required by:
{versionInfo.requiredBy .slice(0, 3) .map( (dep: ProjectDependencyCoordinates) => (
{dep.groupId}:{dep.artifactId}: {dep.versionId}
), )} {versionInfo.requiredBy.length > 3 && (
...and {versionInfo.requiredBy.length - 3}{' '} more
)}
); }, )}
); })}
{resolutionResult.suggestedOverrides && resolutionResult.suggestedOverrides.length > 0 && (
💡 Suggested Version Overrides ( {resolutionResult.suggestedOverrides.length})
These versions may help resolve conflicts. Consider manually updating your dependencies to these versions.
{resolutionResult.suggestedOverrides.map( (override: ProjectDependencyCoordinates) => (
{override.groupId}:{override.artifactId}
→ {override.versionId}
), )}
)}
)}
); }, ); const ProjectDependencyReportModal = observer( (props: { dependencyEditorState: ProjectDependencyEditorState; tab: DEPENDENCY_REPORT_TAB; }) => { const { dependencyEditorState } = props; const applicationStore = dependencyEditorState.editorStore.applicationStore; const reportTab = dependencyEditorState.reportTab; const tabs = Object.values(DEPENDENCY_REPORT_TAB); const changeTab = (tab: DEPENDENCY_REPORT_TAB): (() => void) => (): void => dependencyEditorState.setReportTab(tab); const dependencyReport = dependencyEditorState.dependencyReport; const closeModal = (): void => dependencyEditorState.setReportTab(undefined); const [flattenView, setFlattenView] = useState(false); const [isExpandingDependencies, setIsExpandingDependencies] = useState(false); const setTreeData = ( val: TreeData, ): void => { dependencyEditorState.setTreeData(val, flattenView); }; const toggleViewAsListOrAsTree = (): void => { setFlattenView(!flattenView); }; const treeData = flattenView ? dependencyEditorState.flattenDependencyTreeData : dependencyEditorState.dependencyTreeData; const collapseTree = (): void => { if (treeData) { collapseTreeData(treeData); setTreeData({ ...treeData }); } }; const openAllDependencyNodes = (): void => { if (treeData && dependencyReport) { setIsExpandingDependencies(true); openAllDependencyNodesInTree(treeData, dependencyReport.graph); setTreeData({ ...treeData }); setIsExpandingDependencies(false); } }; return (
{tabs.map((tab) => ( ))}
{reportTab === DEPENDENCY_REPORT_TAB.EXPLORER && (
explorer
{!flattenView && ( <> )}
{flattenView ? ( ) : ( )}
{treeData && ( )}
)} {reportTab === DEPENDENCY_REPORT_TAB.CONFLICTS && dependencyReport && ( )} {reportTab === DEPENDENCY_REPORT_TAB.RESOLUTION && ( )}
); }, ); interface TransitiveDependencyOption { label: string; value: string; groupId: string; artifactId: string; } const ProjectDependencyInlineExclusionsSelector = observer( (props: { projectDependency: ProjectDependency; isReadOnly: boolean }) => { const { projectDependency, isReadOnly } = props; const editorStore = useEditorStore(); const applicationStore = useApplicationStore(); const dependencyEditorState = editorStore.projectConfigurationEditorState.projectDependencyEditorState; const [selectedTransitiveDependency, setSelectedTransitiveDependency] = useState(null); const [transitiveDependencyOptions, setTransitiveDependencyOptions] = useState([]); const getTransitiveDependencies = useCallback((): TransitiveDependencyOption[] => { const dependencyReport = dependencyEditorState.dependencyReport; if (!dependencyReport?.graph) { return []; } const transitiveDeps = new Map(); const existingExclusionCoordinates = dependencyEditorState.getExclusionCoordinates( projectDependency.projectId, ); const visitedNodes = new Set(); const currentProjectCoordinate = generateGAVCoordinates( editorStore.projectConfigurationEditorState .currentProjectConfiguration.groupId, editorStore.projectConfigurationEditorState .currentProjectConfiguration.artifactId, undefined, ); const traverseNode = (nodeId: string) => { if (visitedNodes.has(nodeId)) { return; } visitedNodes.add(nodeId); const node = dependencyReport.graph.nodes.get(nodeId); if (node?.dependencies) { for (let i = 0; i < node.dependencies.length; i++) { const dep = node.dependencies[i]; if (!dep?.groupId || !dep.artifactId) { continue; } const coordinate = generateGAVCoordinates( dep.groupId, dep.artifactId, undefined, ); if ( existingExclusionCoordinates.indexOf(coordinate) === -1 && coordinate !== `${projectDependency.groupId}:${projectDependency.artifactId}` && coordinate !== currentProjectCoordinate ) { transitiveDeps.set(coordinate, { label: generateGAVCoordinates( dep.groupId, dep.artifactId, undefined, ), value: coordinate, groupId: dep.groupId, artifactId: dep.artifactId, }); } traverseNode(dep.id); } } }; const rootNodeId = generateGAVCoordinates( guaranteeNonNullable(projectDependency.groupId), guaranteeNonNullable(projectDependency.artifactId), guaranteeNonNullable(projectDependency.versionId), ); traverseNode(rootNodeId); return Array.from(transitiveDeps.values()).sort((a, b) => a.label.localeCompare(b.label), ); }, [ dependencyEditorState, projectDependency.projectId, projectDependency.groupId, projectDependency.artifactId, projectDependency.versionId, editorStore.projectConfigurationEditorState.currentProjectConfiguration .groupId, editorStore.projectConfigurationEditorState.currentProjectConfiguration .artifactId, ]); useEffect(() => { setTransitiveDependencyOptions(getTransitiveDependencies()); }, [ dependencyEditorState.dependencyReport, projectDependency.projectId, getTransitiveDependencies, ]); const addExclusionFromDropdown = ( option: TransitiveDependencyOption | null, ): void => { if (!option) { return; } try { dependencyEditorState.addExclusionByCoordinate( projectDependency.projectId, option.value, ); setSelectedTransitiveDependency(null); setTransitiveDependencyOptions(getTransitiveDependencies()); applicationStore.notificationService.notifySuccess( `Exclusion added: ${option.value}. Click "Validate Dependencies" to apply changes.`, ); } catch (error) { assertErrorThrown(error); applicationStore.notificationService.notifyError( `Failed to add exclusion: ${error.message}`, ); } }; if (isReadOnly) { return null; } return (
); }, ); const ProjectDependencyExclusionsList = observer( (props: { projectDependency: ProjectDependency; isReadOnly: boolean }) => { const { projectDependency, isReadOnly } = props; const editorStore = useEditorStore(); const applicationStore = useApplicationStore(); const dependencyEditorState = editorStore.projectConfigurationEditorState.projectDependencyEditorState; const [, setForceUpdate] = useState(0); const exclusions = dependencyEditorState.getExclusions( projectDependency.projectId, ); useEffect(() => { setForceUpdate((prev) => prev + 1); }, [ dependencyEditorState.dependencyReport, projectDependency.projectId, dependencyEditorState, ]); useEffect(() => { const interval = setInterval(() => { const currentExclusions = dependencyEditorState.getExclusions( projectDependency.projectId, ); if (currentExclusions.length !== exclusions.length) { setForceUpdate((prev) => prev + 1); } }, 1000); return () => clearInterval(interval); }, [exclusions.length, dependencyEditorState, projectDependency.projectId]); const removeExclusion = (exclusion: ProjectDependencyExclusion): void => { try { dependencyEditorState.removeExclusion( projectDependency.projectId, exclusion, ); if (!dependencyEditorState.hasAnyExclusions) { applicationStore.notificationService.notifySuccess( `Last exclusion removed: ${exclusion.coordinate}. Refreshing dependency tree...`, ); flowResult(dependencyEditorState.fetchDependencyReport()).catch( applicationStore.alertUnhandledError, ); } else { applicationStore.notificationService.notifySuccess( `Exclusion removed: ${exclusion.coordinate}. Click "Validate Dependencies" to apply changes.`, ); } } catch (error) { assertErrorThrown(error); applicationStore.notificationService.notifyError( `Failed to remove exclusion: ${error.message}`, ); } }; if (exclusions.length === 0) { return null; } return (
Exclusions ({exclusions.length})
{exclusions.map((exclusion) => (
{exclusion.coordinate}
{!isReadOnly && ( )}
))}
); }, ); const ProjectVersionDependencyEditor = observer( (props: { projectDependency: ProjectDependency; deleteValue: () => void; isReadOnly: boolean; projects: Map; }) => { const { projectDependency, deleteValue, isReadOnly, projects } = props; const projectDependencyData = projects.get(projectDependency.projectId); const editorStore = useEditorStore(); const applicationStore = useApplicationStore(); const projectSelectorRef = useRef(null); const versionSelectorRef = useRef(null); const configState = editorStore.projectConfigurationEditorState; const dependencyEditorState = configState.projectDependencyEditorState; const versions: string[] = configState.versions.get(projectDependency.projectId) ?? []; const [fetchSelectedProjectVersionsStatus] = useState(ActionState.create()); // project const selectedProject = configState.projects.get( projectDependency.projectId, ); const selectedProjectOption = selectedProject ? buildProjectOption(selectedProject) : null; const projectDisabled = !configState.associatedProjectsAndVersionsFetched || configState.isReadOnly; const currentProjectCoordinates = generateGAVCoordinates( configState.currentProjectConfiguration.groupId, configState.currentProjectConfiguration.artifactId, undefined, ); const projectsOptions = Array.from(configState.projects.values()) .filter((project) => project.coordinates !== currentProjectCoordinates) .map(buildProjectOption) .sort(compareLabelFn); const onProjectSelectionChange = async ( val: ProjectOption | null, ): Promise => { if ( (val !== null || selectedProjectOption !== null) && (!val || !selectedProjectOption || val.value !== selectedProjectOption.value) ) { projectDependency.setProjectId(val?.value.coordinates ?? ''); projectDependency.setVersionId(''); if (val) { try { fetchSelectedProjectVersionsStatus.inProgress(); const _versions = await editorStore.depotServerClient.getVersions( guaranteeNonNullable(projectDependency.groupId), guaranteeNonNullable(projectDependency.artifactId), true, ); configState.versions.set(val.value.coordinates, _versions); if (_versions.length) { projectDependency.setVersionId( guaranteeNonNullable(_versions[_versions.length - 1]), ); flowResult(dependencyEditorState.fetchDependencyReport()).catch( applicationStore.alertUnhandledError, ); } else { projectDependency.setVersionId(''); } fetchSelectedProjectVersionsStatus.complete(); } catch (error) { assertErrorThrown(error); editorStore.applicationStore.notificationService.notifyError(error); fetchSelectedProjectVersionsStatus.reset(); } } } }; // version const version = projectDependency.versionId; const versionOptions = versions .toSorted((v1, v2) => compareSemVerVersions(v2, v1)) .map((v) => { if (v === MASTER_SNAPSHOT_ALIAS) { return { value: v, label: SNAPSHOT_VERSION_ALIAS }; } return { value: v, label: v }; }); const selectedVersionOption: VersionOption | null = versionOptions.find((v) => v.value === version) ?? null; const versionDisabled = Boolean(!versions.length || !projectDependency.projectId.length) || !configState.associatedProjectsAndVersionsFetched || isReadOnly; const onVersionSelectionChange = (val: VersionOption | null): void => { if ( (val !== null || selectedVersionOption !== null) && (!val || !selectedVersionOption || val.value !== selectedVersionOption.value) ) { try { projectDependency.setVersionId(val?.value ?? ''); flowResult(dependencyEditorState.fetchDependencyReport()).catch( applicationStore.alertUnhandledError, ); } catch (error) { assertErrorThrown(error); applicationStore.logService.error( LogEvent.create(LEGEND_STUDIO_APP_EVENT.SDLC_MANAGER_FAILURE), error, ); } } }; const viewProject = (): void => { applicationStore.navigationService.navigator.visitAddress( applicationStore.navigationService.navigator.generateAddress( generateViewProjectByGAVRoute( guaranteeNonNullable(projectDependency.groupId), guaranteeNonNullable(projectDependency.artifactId), projectDependency.versionId === MASTER_SNAPSHOT_ALIAS ? SNAPSHOT_VERSION_ALIAS : projectDependency.versionId, ), ), ); }; // NOTE: This assumes that the dependant project is in the same studio instance as the current project // In the future, the studio instance may be part of the project data const viewSDLCProject = (): void => { if (projectDependencyData) { applicationStore.navigationService.navigator.visitAddress( applicationStore.navigationService.navigator.generateAddress( projectDependency.versionId === MASTER_SNAPSHOT_ALIAS ? generateViewProjectRoute(projectDependencyData.projectId) : generateViewVersionRoute( projectDependencyData.projectId, projectDependency.versionId, ), ), ); } }; const projectSelectorPlaceholder = !projectDependency.projectId.length ? 'Choose project' : versionDisabled ? 'No project version found. Please create a new one.' : 'Select version'; return (
{ onProjectSelectionChange(val).catch( applicationStore.alertUnhandledError, ); }} value={selectedProjectOption} isLoading={configState.fetchingProjectVersionsState.isInProgress} formatOptionLabel={formatOptionLabel} darkMode={ !applicationStore.layoutService.TEMPORARY__isLightColorThemeEnabled } /> {selectedProject && selectedVersionOption && ( )} Project SDLC project } > Go to...
); }, ); export const ProjectDependencyEditor = observer(() => { const editorStore = useEditorStore(); const applicationStore = useApplicationStore(); const configState = editorStore.tabManagerState.getCurrentEditorState( ProjectConfigurationEditorState, ); const dependencyEditorState = configState.projectDependencyEditorState; const currentProjectConfiguration = configState.currentProjectConfiguration; const deleteProjectDependency = (val: ProjectDependency): (() => void) => (): void => { currentProjectConfiguration.deleteProjectDependency(val); flowResult(dependencyEditorState.fetchDependencyReport()).catch( applicationStore.alertUnhandledError, ); }; const isReadOnly = editorStore.isInViewerMode; const isLoading = configState.updatingConfigurationState.isInProgress || configState.fetchingProjectVersionsState.isInProgress || dependencyEditorState.fetchingDependencyInfoState.isInProgress || dependencyEditorState.validatingDependenciesState.isInProgress; return ( {isLoading && (
{configState.updatingConfigurationState.isInProgress ? `Updating configuration...` : configState.fetchingProjectVersionsState.isInProgress ? `Fetching dependency versions` : dependencyEditorState.validatingDependenciesState.isInProgress ? 'Validating dependencies and compiling...' : dependencyEditorState.fetchingDependencyInfoState.isInProgress ? 'Updating project dependency tree and potential conflicts' : ''}
)} {currentProjectConfiguration.projectDependencies.map( (projectDependency) => (
{/* Indented exclusions list */}
), )} {dependencyEditorState.reportTab && ( )}
); });