diff --git a/src/Frontend/Components/Icons/Icons.tsx b/src/Frontend/Components/Icons/Icons.tsx index efb6e6b58..ba49c66bc 100644 --- a/src/Frontend/Components/Icons/Icons.tsx +++ b/src/Frontend/Components/Icons/Icons.tsx @@ -24,6 +24,7 @@ import { } from '../../shared-styles'; import { SxProps } from '@mui/material'; import RectangleIcon from '@mui/icons-material/Rectangle'; +import { Criticality } from '../../../shared/shared-types'; const classes = { clickableIcon, @@ -49,10 +50,26 @@ const classes = { }, }; +const criticalityTooltipText = { + high: 'has high criticality signals', + medium: 'has medium criticality signals', + undefined: 'has signals', +}; + +const criticalityColor = { + high: OpossumColors.orange, + medium: OpossumColors.mediumOrange, + undefined: OpossumColors.darkBlue, +}; + interface IconProps { sx?: SxProps; } +interface SignalIconProps { + criticality?: Criticality; +} + interface LabelDetailIconProps extends IconProps { labelDetail?: string; disabled?: boolean; @@ -102,12 +119,18 @@ export function FollowUpIcon(props: IconProps): ReactElement { ); } -export function SignalIcon(): ReactElement { +export function SignalIcon(props: SignalIconProps): ReactElement { return ( - + ); diff --git a/src/Frontend/Components/Icons/__tests__/Icons.test.tsx b/src/Frontend/Components/Icons/__tests__/Icons.test.tsx index 3c6b1d8c7..8b04bad6f 100644 --- a/src/Frontend/Components/Icons/__tests__/Icons.test.tsx +++ b/src/Frontend/Components/Icons/__tests__/Icons.test.tsx @@ -3,15 +3,21 @@ // // SPDX-License-Identifier: Apache-2.0 -import { render, screen } from '@testing-library/react'; +import { act, fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; +import { Criticality } from '../../../../shared/shared-types'; import { + BreakpointIcon, CommentIcon, DirectoryIcon, ExcludeFromNoticeIcon, FileIcon, FirstPartyIcon, FollowUpIcon, + IncompletePackagesIcon, + PreSelectedIcon, + SearchPackagesIcon, + SignalIcon, } from '../Icons'; describe('The Icons', () => { @@ -50,4 +56,70 @@ describe('The Icons', () => { expect(screen.getByLabelText('File icon')); }); + + it('renders SignalIcon', () => { + render(); + + expect(screen.getByLabelText('Signal icon')); + }); + + it('renders BreakpointIcon', () => { + render(); + + expect(screen.getByLabelText('Breakpoint icon')); + }); + + it('renders IncompletePackagesIcon', () => { + render(); + + expect(screen.getByLabelText('Incomplete icon')); + }); + + it('renders PreSelectedIcon', () => { + render(); + + expect(screen.getByLabelText('Pre-selected icon')); + }); + + it('renders SearchPackagesIcon', () => { + render(); + + expect(screen.getByLabelText('Search packages icon')); + }); +}); + +describe('The SignalIcon', () => { + jest.useFakeTimers(); + it('renders high criticality SignalIcon', () => { + render(); + + const icon = screen.getByLabelText('Signal icon'); + fireEvent.mouseOver(icon); + act(() => { + jest.runAllTimers(); + }); + screen.getByText('has high criticality signals'); + }); + + it('renders medium criticality SignalIcon', () => { + render(); + + const icon = screen.getByLabelText('Signal icon'); + fireEvent.mouseOver(icon); + act(() => { + jest.runAllTimers(); + }); + screen.getByText('has medium criticality signals'); + }); + + it('renders no criticality SignalIcon', () => { + render(); + + const icon = screen.getByLabelText('Signal icon'); + fireEvent.mouseOver(icon); + act(() => { + jest.runAllTimers(); + }); + screen.getByText('has signals'); + }); }); diff --git a/src/Frontend/Components/ResourceBrowser/ResourceBrowser.tsx b/src/Frontend/Components/ResourceBrowser/ResourceBrowser.tsx index df6cc77a4..6446ee360 100644 --- a/src/Frontend/Components/ResourceBrowser/ResourceBrowser.tsx +++ b/src/Frontend/Components/ResourceBrowser/ResourceBrowser.tsx @@ -8,6 +8,7 @@ import React, { ReactElement } from 'react'; import { useAppDispatch, useAppSelector } from '../../state/hooks'; import { getAttributionBreakpoints, + getExternalData, getFilesWithChildren, getManualAttributions, getResources, @@ -101,7 +102,7 @@ export function ResourceBrowser(): ReactElement | null { const attributionBreakpoints = useAppSelector(getAttributionBreakpoints); const filesWithChildren = useAppSelector(getFilesWithChildren); - + const externalData = useAppSelector(getExternalData); const dispatch = useAppDispatch(); function handleToggle(nodeIdsToExpand: Array): void { @@ -140,7 +141,8 @@ export function ResourceBrowser(): ReactElement | null { resourcesWithManualAttributedChildren, resolvedExternalAttributions, getAttributionBreakpointCheck(attributionBreakpoints), - getFileWithChildrenCheck(filesWithChildren) + getFileWithChildrenCheck(filesWithChildren), + externalData ); } diff --git a/src/Frontend/Components/ResourceBrowser/__tests__/get-tree-item-label.test.ts b/src/Frontend/Components/ResourceBrowser/__tests__/get-tree-item-label.test.ts new file mode 100644 index 000000000..a9451bacf --- /dev/null +++ b/src/Frontend/Components/ResourceBrowser/__tests__/get-tree-item-label.test.ts @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +import { Criticality } from '../../../../shared/shared-types'; +import { getCriticality } from '../get-tree-item-label'; + +describe('Tree item labels', () => { + it('checks resource getCriticality', () => { + const resourcesToExternalAttributions = { + '/test_file1.ts': ['attr1', 'attr2'], + '/test_file2.ts': ['attr3'], + '/test_file3.ts': ['attr2', 'attr3'], + }; + const externalAttributions = { + attr1: { criticality: Criticality.High }, + attr2: { criticality: Criticality.Medium }, + attr3: {}, + }; + const expectedCriticalities: { + [resource: string]: Criticality | undefined; + } = { + '/test_file1.ts': Criticality.High, + '/test_file2.ts': undefined, + '/test_file3.ts': Criticality.Medium, + }; + + for (const nodeId of Object.keys(resourcesToExternalAttributions)) { + const criticality = getCriticality( + nodeId, + resourcesToExternalAttributions, + externalAttributions + ); + expect(criticality).toEqual(expectedCriticalities[nodeId]); + } + }); +}); diff --git a/src/Frontend/Components/ResourceBrowser/get-tree-item-label.tsx b/src/Frontend/Components/ResourceBrowser/get-tree-item-label.tsx index 64986d284..3b3759e3e 100644 --- a/src/Frontend/Components/ResourceBrowser/get-tree-item-label.tsx +++ b/src/Frontend/Components/ResourceBrowser/get-tree-item-label.tsx @@ -4,7 +4,9 @@ // SPDX-License-Identifier: Apache-2.0 import { + AttributionData, Attributions, + Criticality, Resources, ResourcesToAttributions, ResourcesWithAttributedChildren, @@ -25,7 +27,8 @@ export function getTreeItemLabel( resourcesWithManualAttributedChildren: ResourcesWithAttributedChildren, resolvedExternalAttributions: Set, isAttributionBreakpoint: PathPredicate, - isFileWithChildren: PathPredicate + isFileWithChildren: PathPredicate, + externalData: AttributionData ): ReactElement { const canHaveChildren = resource !== 1; @@ -60,6 +63,11 @@ export function getTreeItemLabel( nodeId, resourcesWithManualAttributedChildren )} + criticality={getCriticality( + nodeId, + resourcesToExternalAttributions, + externalData.attributions + )} isAttributionBreakpoint={isAttributionBreakpoint(nodeId)} showFolderIcon={canHaveChildren && !isFileWithChildren(nodeId)} containsResourcesWithOnlyExternalAttribution={ @@ -75,6 +83,32 @@ export function getTreeItemLabel( ); } +export function getCriticality( + nodeId: string, + resourcesToExternalAttributions: ResourcesToAttributions, + externalAttributions: Attributions +): Criticality | undefined { + if (hasExternalAttribution(nodeId, resourcesToExternalAttributions)) { + const attributionsForResource = resourcesToExternalAttributions[nodeId]; + + for (const attribution of attributionsForResource) { + if (externalAttributions[attribution].criticality === Criticality.High) { + return Criticality.High; + } + } + + for (const attribution of attributionsForResource) { + if ( + externalAttributions[attribution].criticality === Criticality.Medium + ) { + return Criticality.Medium; + } + } + + return undefined; + } +} + function isRootResource(resourceName: string): boolean { return resourceName === ''; } diff --git a/src/Frontend/Components/StyledTreeItemLabel/StyledTreeItemLabel.tsx b/src/Frontend/Components/StyledTreeItemLabel/StyledTreeItemLabel.tsx index 639e2ee42..1988ce698 100644 --- a/src/Frontend/Components/StyledTreeItemLabel/StyledTreeItemLabel.tsx +++ b/src/Frontend/Components/StyledTreeItemLabel/StyledTreeItemLabel.tsx @@ -14,6 +14,7 @@ import { import { OpossumColors, tooltipStyle } from '../../shared-styles'; import { SxProps } from '@mui/material'; import MuiBox from '@mui/material/Box'; +import { Criticality } from '../../../shared/shared-types'; const classes = { manualIcon: { @@ -83,6 +84,7 @@ interface StyledTreeItemProps { isAttributionBreakpoint: boolean; showFolderIcon: boolean; containsResourcesWithOnlyExternalAttribution: boolean; + criticality?: Criticality; } export function StyledTreeItemLabel(props: StyledTreeItemProps): ReactElement { @@ -145,7 +147,9 @@ export function StyledTreeItemLabel(props: StyledTreeItemProps): ReactElement { > {props.labelText} - {props.hasExternalAttribution ? : null} + {props.hasExternalAttribution ? ( + + ) : null} ); }