From ac46a357d6b665bd11f5289927312a57b013ff0f Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Tue, 25 Feb 2025 18:25:53 +0100 Subject: [PATCH 01/24] feat: add pie chart to project statistics popup showing signal count by classification --- .../ProjectStatisticsPopup.tsx | 30 ++++++++--- .../ProjectStatisticsPopup.util.ts | 54 ++++++++++++++++++- src/Frontend/enums/enums.ts | 1 + src/Frontend/types/types.ts | 4 ++ 4 files changed, 81 insertions(+), 8 deletions(-) diff --git a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx index 9c73788eb..a9b1efe0a 100644 --- a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx +++ b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx @@ -10,6 +10,7 @@ import { ProjectStatisticsPopupTitle } from '../../enums/enums'; import { closePopup } from '../../state/actions/view-actions/view-actions'; import { useAppDispatch, useAppSelector } from '../../state/hooks'; import { + getClassifications, getExternalAttributionSources, getManualAttributions, getUnresolvedExternalAttributions, @@ -27,6 +28,7 @@ import { getCriticalSignalsCount, getIncompleteAttributionsCount, getMostFrequentLicenses, + getSignalCountByClassification, getUniqueLicenseNameToAttribution, } from './ProjectStatisticsPopup.util'; @@ -41,6 +43,7 @@ export const ProjectStatisticsPopup: React.FC = () => { const manualAttributions = useAppSelector(getManualAttributions); const attributionSources = useAppSelector(getExternalAttributionSources); + const classifications = useAppSelector(getClassifications); const unresolvedExternalAttribution = useAppSelector( getUnresolvedExternalAttributions, @@ -50,12 +53,15 @@ export const ProjectStatisticsPopup: React.FC = () => { unresolvedExternalAttribution, ); - const { licenseCounts, licenseNamesWithCriticality } = - aggregateLicensesAndSourcesFromAttributions( - unresolvedExternalAttribution, - strippedLicenseNameToAttribution, - attributionSources, - ); + const { + licenseCounts, + licenseNamesWithCriticality, + licenseNamesWithClassification, + } = aggregateLicensesAndSourcesFromAttributions( + unresolvedExternalAttribution, + strippedLicenseNameToAttribution, + attributionSources, + ); const mostFrequentLicenseCountData = getMostFrequentLicenses(licenseCounts); @@ -64,6 +70,12 @@ export const ProjectStatisticsPopup: React.FC = () => { licenseNamesWithCriticality, ); + const signalCountByClassification = getSignalCountByClassification( + licenseCounts, + licenseNamesWithClassification, + classifications, + ); + const manualAttributionPropertyCounts = aggregateAttributionPropertiesFromAttributions(manualAttributions); @@ -121,6 +133,12 @@ export const ProjectStatisticsPopup: React.FC = () => { data={criticalSignalsCountData} title={ProjectStatisticsPopupTitle.CriticalSignalsCountPieChart} /> + // // SPDX-License-Identifier: Apache-2.0 -import { pickBy } from 'lodash'; +import { pickBy, toNumber } from 'lodash'; import { Attributions, + Classifications, Criticality, ExternalAttributionSources, PackageInfo, @@ -14,6 +15,7 @@ import { PieChartCriticalityNames } from '../../enums/enums'; import { AttributionCountPerSourcePerLicense, LicenseCounts, + LicenseNamesWithClassification, LicenseNamesWithCriticality, PieChartData, } from '../../types/types'; @@ -39,11 +41,13 @@ export function aggregateLicensesAndSourcesFromAttributions( ): { licenseCounts: LicenseCounts; licenseNamesWithCriticality: LicenseNamesWithCriticality; + licenseNamesWithClassification: LicenseNamesWithClassification; } { const { attributionCountPerSourcePerLicense, totalAttributionsPerLicense, licenseNamesWithCriticality, + licenseNamesWithClassification, } = getLicenseDataFromAttributionsAndSources( strippedLicenseNameToAttribution, attributions, @@ -67,7 +71,11 @@ export function aggregateLicensesAndSourcesFromAttributions( totalAttributionsPerSource, }; - return { licenseCounts, licenseNamesWithCriticality }; + return { + licenseCounts, + licenseNamesWithCriticality, + licenseNamesWithClassification, + }; } function getLicenseDataFromAttributionsAndSources( @@ -78,8 +86,10 @@ function getLicenseDataFromAttributionsAndSources( attributionCountPerSourcePerLicense: AttributionCountPerSourcePerLicense; totalAttributionsPerLicense: { [licenseName: string]: number }; licenseNamesWithCriticality: LicenseNamesWithCriticality; + licenseNamesWithClassification: LicenseNamesWithClassification; } { const licenseNamesWithCriticality: LicenseNamesWithCriticality = {}; + const licenseNamesWithClassification: LicenseNamesWithClassification = {}; const attributionCountPerSourcePerLicense: AttributionCountPerSourcePerLicense = {}; const totalAttributionsPerLicense: { [licenseName: string]: number } = {}; @@ -90,6 +100,7 @@ function getLicenseDataFromAttributionsAndSources( const { mostFrequentLicenseName, licenseCriticality, + licenseClassification, sourcesCountForLicense, } = getLicenseDataFromVariants( strippedLicenseNameToAttribution[strippedLicenseName], @@ -98,6 +109,8 @@ function getLicenseDataFromAttributionsAndSources( ); licenseNamesWithCriticality[mostFrequentLicenseName] = licenseCriticality; + licenseNamesWithClassification[mostFrequentLicenseName] = + licenseClassification; attributionCountPerSourcePerLicense[mostFrequentLicenseName] = sourcesCountForLicense; totalAttributionsPerLicense[mostFrequentLicenseName] = Object.values( @@ -111,6 +124,7 @@ function getLicenseDataFromAttributionsAndSources( attributionCountPerSourcePerLicense, totalAttributionsPerLicense, licenseNamesWithCriticality, + licenseNamesWithClassification, }; } @@ -121,6 +135,7 @@ function getLicenseDataFromVariants( ): { mostFrequentLicenseName: string; licenseCriticality: Criticality | undefined; + licenseClassification: number; sourcesCountForLicense: { [sourceNameOrTotal: string]: number; }; @@ -133,6 +148,8 @@ function getLicenseDataFromVariants( } = {}; const licenseCriticalityCounts = { high: 0, medium: 0, none: 0 }; + let licenseClassification = 0; + for (const attributionId of attributionIds) { const licenseName = attributions[attributionId].licenseName; if (licenseName) { @@ -141,6 +158,11 @@ function getLicenseDataFromVariants( const variantCriticality = attributions[attributionId].criticality; + licenseClassification = Math.max( + licenseClassification, + attributions[attributionId].classification ?? 0, + ); + licenseCriticalityCounts[variantCriticality || 'none']++; const sourceId = @@ -163,6 +185,7 @@ function getLicenseDataFromVariants( return { mostFrequentLicenseName, licenseCriticality, + licenseClassification, sourcesCountForLicense, }; } @@ -327,6 +350,33 @@ export function getCriticalSignalsCount( ); } +export function getSignalCountByClassification( + licenseCounts: LicenseCounts, + licenseNamesWithClassification: LicenseNamesWithClassification, + classifications: Classifications, +): Array { + const classificationCounts: { [classification: number]: number } = {}; + + for (const [license, attributionCount] of Object.entries( + licenseCounts.totalAttributionsPerLicense, + )) { + classificationCounts[licenseNamesWithClassification[license]] = + (classificationCounts[licenseNamesWithClassification[license]] ?? 0) + + attributionCount; + } + + return Object.keys(classifications).map((classification) => { + const classificationName = classifications[toNumber(classification)]; + const classificationCount = + classificationCounts[toNumber(classification)] ?? 0; + + return { + name: classificationName, + count: classificationCount, + }; + }); +} + export function getIncompleteAttributionsCount( attributions: Attributions, ): Array { diff --git a/src/Frontend/enums/enums.ts b/src/Frontend/enums/enums.ts index 6bc89a97b..01297d24b 100644 --- a/src/Frontend/enums/enums.ts +++ b/src/Frontend/enums/enums.ts @@ -31,6 +31,7 @@ export enum ProjectStatisticsPopupTitle { PieChartsSectionHeader = 'Pie Charts', MostFrequentLicenseCountPieChart = 'Most Frequent Licenses', CriticalSignalsCountPieChart = 'Signals by Criticality', + SignalCountByClassificationPieChart = 'Signals by Classification', IncompleteLicensesPieChart = 'Incomplete Attributions', } diff --git a/src/Frontend/types/types.ts b/src/Frontend/types/types.ts index e291242d0..05a7f9d8a 100644 --- a/src/Frontend/types/types.ts +++ b/src/Frontend/types/types.ts @@ -51,3 +51,7 @@ export interface AttributionCountPerSourcePerLicense { export interface LicenseNamesWithCriticality { [licenseName: string]: Criticality | undefined; } + +export interface LicenseNamesWithClassification { + [licenseName: string]: number; +} From a6ae64d2d91db970d3fb6c80f8908d1599dfef59 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Wed, 26 Feb 2025 10:59:28 +0100 Subject: [PATCH 02/24] feat: properly deal with counting signals with undefined classifications --- .../ProjectStatisticsPopup.util.ts | 36 +++++++++++++------ src/Frontend/types/types.ts | 2 +- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.util.ts b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.util.ts index 47448e706..4f698310a 100644 --- a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.util.ts +++ b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.util.ts @@ -135,7 +135,7 @@ function getLicenseDataFromVariants( ): { mostFrequentLicenseName: string; licenseCriticality: Criticality | undefined; - licenseClassification: number; + licenseClassification: number | undefined; sourcesCountForLicense: { [sourceNameOrTotal: string]: number; }; @@ -148,7 +148,7 @@ function getLicenseDataFromVariants( } = {}; const licenseCriticalityCounts = { high: 0, medium: 0, none: 0 }; - let licenseClassification = 0; + let licenseClassification: number | undefined = undefined; for (const attributionId of attributionIds) { const licenseName = attributions[attributionId].licenseName; @@ -158,10 +158,16 @@ function getLicenseDataFromVariants( const variantCriticality = attributions[attributionId].criticality; - licenseClassification = Math.max( - licenseClassification, - attributions[attributionId].classification ?? 0, - ); + const variantClassification = attributions[attributionId].classification; + + if (licenseClassification === undefined) { + licenseClassification = variantClassification; + } else if (variantClassification !== undefined) { + licenseClassification = Math.max( + licenseClassification, + attributions[attributionId].classification ?? 0, + ); + } licenseCriticalityCounts[variantCriticality || 'none']++; @@ -360,12 +366,13 @@ export function getSignalCountByClassification( for (const [license, attributionCount] of Object.entries( licenseCounts.totalAttributionsPerLicense, )) { - classificationCounts[licenseNamesWithClassification[license]] = - (classificationCounts[licenseNamesWithClassification[license]] ?? 0) + - attributionCount; + // count undefined classification at index -1 in classificationCounts + const classification = licenseNamesWithClassification[license] ?? -1; + classificationCounts[classification] = + (classificationCounts[classification] ?? 0) + attributionCount; } - return Object.keys(classifications).map((classification) => { + const pieChartData = Object.keys(classifications).map((classification) => { const classificationName = classifications[toNumber(classification)]; const classificationCount = classificationCounts[toNumber(classification)] ?? 0; @@ -375,6 +382,15 @@ export function getSignalCountByClassification( count: classificationCount, }; }); + + if (classificationCounts[-1]) { + return pieChartData.concat({ + name: 'No Classification', + count: classificationCounts[-1], + }); + } + + return pieChartData; } export function getIncompleteAttributionsCount( diff --git a/src/Frontend/types/types.ts b/src/Frontend/types/types.ts index 05a7f9d8a..757ec497f 100644 --- a/src/Frontend/types/types.ts +++ b/src/Frontend/types/types.ts @@ -53,5 +53,5 @@ export interface LicenseNamesWithCriticality { } export interface LicenseNamesWithClassification { - [licenseName: string]: number; + [licenseName: string]: number | undefined; } From 5aa5c532c00f83e6f318196ee8c0c88b72914f4f Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Wed, 26 Feb 2025 11:13:32 +0100 Subject: [PATCH 03/24] feat: exclude classifications with no associated signals from pie chart --- .../ProjectStatisticsPopup.util.ts | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.util.ts b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.util.ts index 4f698310a..120594092 100644 --- a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.util.ts +++ b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.util.ts @@ -372,16 +372,18 @@ export function getSignalCountByClassification( (classificationCounts[classification] ?? 0) + attributionCount; } - const pieChartData = Object.keys(classifications).map((classification) => { - const classificationName = classifications[toNumber(classification)]; - const classificationCount = - classificationCounts[toNumber(classification)] ?? 0; - - return { - name: classificationName, - count: classificationCount, - }; - }); + const pieChartData = Object.keys(classifications) + .map((classification) => { + const classificationName = classifications[toNumber(classification)]; + const classificationCount = + classificationCounts[toNumber(classification)] ?? 0; + + return { + name: classificationName, + count: classificationCount, + }; + }) + .filter(({ count }) => count > 0); if (classificationCounts[-1]) { return pieChartData.concat({ From a4f6edae9aff9cb8e1997214b92ad40da45ab3fe Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Wed, 26 Feb 2025 11:14:29 +0100 Subject: [PATCH 04/24] test: update test for project statistics popup --- .../input/__tests__/importFromFile.test.ts | 16 +++++++-------- .../input/__tests__/parseFile.test.ts | 4 ++-- .../__tests__/ProjectStatisticsPopup.test.tsx | 20 ++++++++++++++++++- .../__tests__/load-actions.test.ts | 4 ++-- .../test-helpers/general-test-helpers.ts | 3 +++ 5 files changed, 34 insertions(+), 13 deletions(-) diff --git a/src/ElectronBackend/input/__tests__/importFromFile.test.ts b/src/ElectronBackend/input/__tests__/importFromFile.test.ts index 73c375c32..c302eacd0 100644 --- a/src/ElectronBackend/input/__tests__/importFromFile.test.ts +++ b/src/ElectronBackend/input/__tests__/importFromFile.test.ts @@ -68,8 +68,8 @@ const inputFileContent: ParsedOpossumInputFile = { }, config: { classifications: { - 0: 'UNKNOWN', - 1: 'CRITICAL', + 0: 'GOOD', + 1: 'BAD', }, }, externalAttributions: { @@ -115,8 +115,8 @@ const expectedFileContent: ParsedFileContent = { resources: { a: 1, folder: {} }, config: { classifications: { - 0: 'UNKNOWN', - 1: 'CRITICAL', + 0: 'GOOD', + 1: 'BAD', }, }, manualAttributions: { @@ -376,8 +376,8 @@ describe('Test of loading function', () => { }, config: { classifications: { - 0: 'UNKNOWN', - 1: 'CRITICAL', + 0: 'GOOD', + 1: 'BAD', }, }, externalAttributions: { @@ -438,8 +438,8 @@ describe('Test of loading function', () => { resources: { a: 1 }, config: { classifications: { - 0: 'UNKNOWN', - 1: 'CRITICAL', + 0: 'GOOD', + 1: 'BAD', }, }, manualAttributions: { diff --git a/src/ElectronBackend/input/__tests__/parseFile.test.ts b/src/ElectronBackend/input/__tests__/parseFile.test.ts index 1fa89da99..a65ea9c65 100644 --- a/src/ElectronBackend/input/__tests__/parseFile.test.ts +++ b/src/ElectronBackend/input/__tests__/parseFile.test.ts @@ -27,8 +27,8 @@ const correctInput: ParsedOpossumInputFile = { }, config: { classifications: { - 0: 'UNKNOWN', - 1: 'CRITICAL', + 0: 'GOOD', + 1: 'BAD', }, }, externalAttributions: { diff --git a/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx b/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx index 6ba6c9521..fa71ab5f4 100644 --- a/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx +++ b/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx @@ -91,6 +91,7 @@ describe('The ProjectStatisticsPopup', () => { actions: [ loadFromFile( getParsedInputFileEnrichedWithTestData({ + config: { classifications: { 0: 'GOOD', 1: 'BAD' } }, manualAttributions: testManualAttributions, externalAttributions: testExternalAttributions, }), @@ -108,6 +109,11 @@ describe('The ProjectStatisticsPopup', () => { ProjectStatisticsPopupTitle.CriticalSignalsCountPieChart, ), ).toBeInTheDocument(); + expect( + screen.getByText( + ProjectStatisticsPopupTitle.SignalCountByClassificationPieChart, + ), + ).toBeInTheDocument(); expect( screen.getAllByText( ProjectStatisticsPopupTitle.IncompleteLicensesPieChart, @@ -121,6 +127,7 @@ describe('The ProjectStatisticsPopup', () => { actions: [ loadFromFile( getParsedInputFileEnrichedWithTestData({ + config: { classifications: { 0: 'GOOD', 1: 'BAD' } }, externalAttributions: testExternalAttributions, }), ), @@ -136,6 +143,11 @@ describe('The ProjectStatisticsPopup', () => { ProjectStatisticsPopupTitle.CriticalSignalsCountPieChart, ), ).not.toBeInTheDocument(); + expect( + screen.queryByText( + ProjectStatisticsPopupTitle.SignalCountByClassificationPieChart, + ), + ).not.toBeInTheDocument(); expect( screen.getByText(ProjectStatisticsPopupTitle.IncompleteLicensesPieChart), ).toBeInTheDocument(); @@ -146,7 +158,7 @@ describe('The ProjectStatisticsPopup', () => { ).not.toHaveLength(2); }); - it('renders pie charts pie charts related to signals even if there are no attributions', () => { + it('renders pie charts related to signals even if there are no attributions', () => { const testManualAttributions: Attributions = {}; const testExternalAttributions: Attributions = { uuid_1: { @@ -170,6 +182,7 @@ describe('The ProjectStatisticsPopup', () => { actions: [ loadFromFile( getParsedInputFileEnrichedWithTestData({ + config: { classifications: { 0: 'GOOD', 1: 'BAD' } }, manualAttributions: testManualAttributions, externalAttributions: testExternalAttributions, }), @@ -186,6 +199,11 @@ describe('The ProjectStatisticsPopup', () => { ProjectStatisticsPopupTitle.CriticalSignalsCountPieChart, ), ).toBeInTheDocument(); + expect( + screen.getByText( + ProjectStatisticsPopupTitle.SignalCountByClassificationPieChart, + ), + ).toBeInTheDocument(); expect( screen.getByText(ProjectStatisticsPopupTitle.IncompleteLicensesPieChart), ).toBeInTheDocument(); diff --git a/src/Frontend/state/actions/resource-actions/__tests__/load-actions.test.ts b/src/Frontend/state/actions/resource-actions/__tests__/load-actions.test.ts index aede447cb..be9211968 100644 --- a/src/Frontend/state/actions/resource-actions/__tests__/load-actions.test.ts +++ b/src/Frontend/state/actions/resource-actions/__tests__/load-actions.test.ts @@ -49,8 +49,8 @@ const testResources: Resources = { const testConfig: ProjectConfig = { classifications: { - 0: 'UNKNOWN', - 1: 'CRITICAL', + 0: 'GOOD', + 1: 'BAD', }, }; diff --git a/src/Frontend/test-helpers/general-test-helpers.ts b/src/Frontend/test-helpers/general-test-helpers.ts index 7191858fe..96502fa6e 100644 --- a/src/Frontend/test-helpers/general-test-helpers.ts +++ b/src/Frontend/test-helpers/general-test-helpers.ts @@ -10,6 +10,7 @@ import { AttributionsToResources, ExternalAttributionSources, ParsedFileContent, + ProjectConfig, Resources, ResourcesToAttributions, } from '../../shared/shared-types'; @@ -44,6 +45,7 @@ const EMPTY_PARSED_FILE_CONTENT: ParsedFileContent = { export function getParsedInputFileEnrichedWithTestData(testData: { resources?: Resources; + config?: ProjectConfig; manualAttributions?: Attributions; resourcesToManualAttributions?: ResourcesToAttributions; externalAttributions?: Attributions; @@ -76,6 +78,7 @@ export function getParsedInputFileEnrichedWithTestData(testData: { return { ...EMPTY_PARSED_FILE_CONTENT, resources, + ...(testData.config ? { config: testData.config } : {}), manualAttributions: { attributions: testData.manualAttributions || {}, resourcesToAttributions: testResourcesToManualAttributions, From 5dbe24f4d8caf279f330e98ac98dc812407c8fb5 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Wed, 26 Feb 2025 11:49:57 +0100 Subject: [PATCH 05/24] refactor: move text for project statistics charts to text.ts file --- .../AccordionWithPieChart.tsx | 9 ++-- .../__tests__/AccordionWithPieChart.test.tsx | 10 ++--- .../ProjectStatisticsPopup.tsx | 26 +++++++---- .../__tests__/ProjectStatisticsPopup.test.tsx | 45 ++++++++----------- src/Frontend/enums/enums.ts | 11 ----- src/shared/text.ts | 10 +++++ 6 files changed, 54 insertions(+), 57 deletions(-) diff --git a/src/Frontend/Components/AccordionWithPieChart/AccordionWithPieChart.tsx b/src/Frontend/Components/AccordionWithPieChart/AccordionWithPieChart.tsx index 1fd04c90f..fdd6dd86c 100644 --- a/src/Frontend/Components/AccordionWithPieChart/AccordionWithPieChart.tsx +++ b/src/Frontend/Components/AccordionWithPieChart/AccordionWithPieChart.tsx @@ -8,10 +8,8 @@ import MuiAccordionDetails from '@mui/material/AccordionDetails'; import MuiAccordionSummary from '@mui/material/AccordionSummary'; import MuiTypography from '@mui/material/Typography'; -import { - PieChartCriticalityNames, - ProjectStatisticsPopupTitle, -} from '../../enums/enums'; +import { text } from '../../../shared/text'; +import { PieChartCriticalityNames } from '../../enums/enums'; import { OpossumColors } from '../../shared-styles'; import { PieChartData } from '../../types/types'; import { PieChart } from '../PieChart/PieChart'; @@ -46,7 +44,8 @@ export function getColorsForPieChart( const pieChartColors: Array = []; if ( - pieChartTitle === ProjectStatisticsPopupTitle.CriticalSignalsCountPieChart + pieChartTitle === + text.projectStatisticsPopup.charts.criticalSignalsCountPieChart ) { for (const pieChartSegment of pieChartData) { switch (pieChartSegment.name) { diff --git a/src/Frontend/Components/AccordionWithPieChart/__tests__/AccordionWithPieChart.test.tsx b/src/Frontend/Components/AccordionWithPieChart/__tests__/AccordionWithPieChart.test.tsx index 604781f59..cca9f2382 100644 --- a/src/Frontend/Components/AccordionWithPieChart/__tests__/AccordionWithPieChart.test.tsx +++ b/src/Frontend/Components/AccordionWithPieChart/__tests__/AccordionWithPieChart.test.tsx @@ -2,10 +2,8 @@ // SPDX-FileCopyrightText: TNG Technology Consulting GmbH // // SPDX-License-Identifier: Apache-2.0 -import { - PieChartCriticalityNames, - ProjectStatisticsPopupTitle, -} from '../../../enums/enums'; +import { text } from '../../../../shared/text'; +import { PieChartCriticalityNames } from '../../../enums/enums'; import { OpossumColors } from '../../../shared-styles'; import { PieChartData } from '../../../types/types'; import { getColorsForPieChart } from '../AccordionWithPieChart'; @@ -34,7 +32,7 @@ describe('getColorsForPieChart', () => { const pieChartColors = getColorsForPieChart( criticalSignalsCount, - ProjectStatisticsPopupTitle.CriticalSignalsCountPieChart, + text.projectStatisticsPopup.charts.criticalSignalsCountPieChart, ); expect(pieChartColors).toEqual(expectedPieChartColors); @@ -55,7 +53,7 @@ describe('getColorsForPieChart', () => { const pieChartColors = getColorsForPieChart( sortedMostFrequentLicenses, - ProjectStatisticsPopupTitle.MostFrequentLicenseCountPieChart, + text.projectStatisticsPopup.charts.mostFrequentLicenseCountPieChart, ); expect(pieChartColors).toEqual(expectedPieChartColors); diff --git a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx index a9b1efe0a..39385c745 100644 --- a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx +++ b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx @@ -6,7 +6,6 @@ import MuiBox from '@mui/material/Box'; import MuiTypography from '@mui/material/Typography'; import { text } from '../../../shared/text'; -import { ProjectStatisticsPopupTitle } from '../../enums/enums'; import { closePopup } from '../../state/actions/view-actions/view-actions'; import { useAppDispatch, useAppSelector } from '../../state/hooks'; import { @@ -105,7 +104,8 @@ export const ProjectStatisticsPopup: React.FC = () => { manualAttributionPropertyCounts, )} title={ - ProjectStatisticsPopupTitle.AttributionPropertyCountTable + text.projectStatisticsPopup.charts + .attributionPropertyCountTable } /> { licenseCounts.totalAttributionsPerLicense } licenseNamesWithCriticality={licenseNamesWithCriticality} - title={ProjectStatisticsPopupTitle.CriticalLicensesTable} + title={text.projectStatisticsPopup.charts.criticalLicensesTable} /> {isThereAnyPieChartData - ? ProjectStatisticsPopupTitle.PieChartsSectionHeader + ? text.projectStatisticsPopup.charts.pieChartsSectionHeader : null} } diff --git a/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx b/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx index fa71ab5f4..f9408d023 100644 --- a/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx +++ b/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx @@ -7,7 +7,6 @@ import userEvent from '@testing-library/user-event'; import { Attributions } from '../../../../shared/shared-types'; import { text } from '../../../../shared/text'; -import { ProjectStatisticsPopupTitle } from '../../../enums/enums'; import { loadFromFile } from '../../../state/actions/resource-actions/load-actions'; import { getParsedInputFileEnrichedWithTestData } from '../../../test-helpers/general-test-helpers'; import { renderComponent } from '../../../test-helpers/render'; @@ -49,7 +48,7 @@ describe('The ProjectStatisticsPopup', () => { expect(screen.getByText('Reuser')).toBeInTheDocument(); }); - it('renders pie charts when there are attributions', () => { + it('renders pie charts when there are signals', () => { const testManualAttributions: Attributions = { uuid_1: { source: { @@ -101,27 +100,27 @@ describe('The ProjectStatisticsPopup', () => { expect( screen.getByText( - ProjectStatisticsPopupTitle.MostFrequentLicenseCountPieChart, + text.projectStatisticsPopup.charts.mostFrequentLicenseCountPieChart, ), ).toBeInTheDocument(); expect( screen.getByText( - ProjectStatisticsPopupTitle.CriticalSignalsCountPieChart, + text.projectStatisticsPopup.charts.criticalSignalsCountPieChart, ), ).toBeInTheDocument(); expect( screen.getByText( - ProjectStatisticsPopupTitle.SignalCountByClassificationPieChart, + text.projectStatisticsPopup.charts.signalCountByClassificationPieChart, ), ).toBeInTheDocument(); expect( screen.getAllByText( - ProjectStatisticsPopupTitle.IncompleteLicensesPieChart, + text.projectStatisticsPopup.charts.incompleteAttributionsPieChart, ), ).toHaveLength(2); }); - it('does not render pie charts when there are no attributions', () => { + it('does not render pie charts when there are no signals', () => { const testExternalAttributions: Attributions = {}; renderComponent(, { actions: [ @@ -135,27 +134,24 @@ describe('The ProjectStatisticsPopup', () => { }); expect( screen.queryByText( - ProjectStatisticsPopupTitle.MostFrequentLicenseCountPieChart, + text.projectStatisticsPopup.charts.mostFrequentLicenseCountPieChart, ), ).not.toBeInTheDocument(); expect( screen.queryByText( - ProjectStatisticsPopupTitle.CriticalSignalsCountPieChart, + text.projectStatisticsPopup.charts.criticalSignalsCountPieChart, ), ).not.toBeInTheDocument(); expect( screen.queryByText( - ProjectStatisticsPopupTitle.SignalCountByClassificationPieChart, + text.projectStatisticsPopup.charts.signalCountByClassificationPieChart, ), ).not.toBeInTheDocument(); expect( - screen.getByText(ProjectStatisticsPopupTitle.IncompleteLicensesPieChart), - ).toBeInTheDocument(); - expect( - screen.getAllByText( - ProjectStatisticsPopupTitle.IncompleteLicensesPieChart, + screen.getByText( + text.projectStatisticsPopup.charts.incompleteAttributionsPieChart, ), - ).not.toHaveLength(2); + ).toBeInTheDocument(); }); it('renders pie charts related to signals even if there are no attributions', () => { @@ -191,30 +187,27 @@ describe('The ProjectStatisticsPopup', () => { }); expect( screen.getByText( - ProjectStatisticsPopupTitle.MostFrequentLicenseCountPieChart, + text.projectStatisticsPopup.charts.mostFrequentLicenseCountPieChart, ), ).toBeInTheDocument(); expect( screen.getByText( - ProjectStatisticsPopupTitle.CriticalSignalsCountPieChart, + text.projectStatisticsPopup.charts.criticalSignalsCountPieChart, ), ).toBeInTheDocument(); expect( screen.getByText( - ProjectStatisticsPopupTitle.SignalCountByClassificationPieChart, + text.projectStatisticsPopup.charts.signalCountByClassificationPieChart, ), ).toBeInTheDocument(); expect( - screen.getByText(ProjectStatisticsPopupTitle.IncompleteLicensesPieChart), - ).toBeInTheDocument(); - expect( - screen.getAllByText( - ProjectStatisticsPopupTitle.IncompleteLicensesPieChart, + screen.getByText( + text.projectStatisticsPopup.charts.incompleteAttributionsPieChart, ), - ).not.toHaveLength(2); + ).toBeInTheDocument(); }); - it('renders tables when there are no attributions', () => { + it('renders tables when there are no attributions and no signals', () => { const testExternalAttributions: Attributions = {}; renderComponent(, { actions: [ diff --git a/src/Frontend/enums/enums.ts b/src/Frontend/enums/enums.ts index 01297d24b..6f702d2e7 100644 --- a/src/Frontend/enums/enums.ts +++ b/src/Frontend/enums/enums.ts @@ -24,17 +24,6 @@ export enum ButtonText { Delete = 'Delete', } -export enum ProjectStatisticsPopupTitle { - LicenseCountsTable = 'Signals per Sources', - AttributionPropertyCountTable = 'Attributions Overview', - CriticalLicensesTable = 'Critical Licenses', - PieChartsSectionHeader = 'Pie Charts', - MostFrequentLicenseCountPieChart = 'Most Frequent Licenses', - CriticalSignalsCountPieChart = 'Signals by Criticality', - SignalCountByClassificationPieChart = 'Signals by Classification', - IncompleteLicensesPieChart = 'Incomplete Attributions', -} - export enum PieChartCriticalityNames { HighCriticality = 'Highly critical signals', MediumCriticality = 'Medium critical signals', diff --git a/src/shared/text.ts b/src/shared/text.ts index d50ca0561..984e0af91 100644 --- a/src/shared/text.ts +++ b/src/shared/text.ts @@ -167,6 +167,16 @@ export const text = { title: 'Project Statistics', toggleStartupCheckbox: 'Show project statistics on startup', criticalLicensesSignalCountColumnName: 'Signals Count', + charts: { + licenseCountsTable: 'Signals per Sources', + attributionPropertyCountTable: 'Attributions Overview', + criticalLicensesTable: 'Critical Licenses', + pieChartsSectionHeader: 'Pie Charts', + mostFrequentLicenseCountPieChart: 'Most Frequent Licenses', + criticalSignalsCountPieChart: 'Signals by Criticality', + signalCountByClassificationPieChart: 'Signals by Classification', + incompleteAttributionsPieChart: 'Incomplete Attributions', + }, }, unsavedChangesPopup: { title: 'Unsaved Changes', From 943c40dbe222a371a74a45b0f501bf1858d3bf90 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Wed, 26 Feb 2025 12:08:55 +0100 Subject: [PATCH 06/24] refactor: move color definition for critical signal pie chart out of general pie chart component --- .../AccordionWithPieChart.tsx | 31 ++-------- .../__tests__/AccordionWithPieChart.test.tsx | 61 ------------------- .../ProjectStatisticsPopup.tsx | 10 +++ .../ProjectStatisticsPopup.util.ts | 4 +- 4 files changed, 17 insertions(+), 89 deletions(-) delete mode 100644 src/Frontend/Components/AccordionWithPieChart/__tests__/AccordionWithPieChart.test.tsx diff --git a/src/Frontend/Components/AccordionWithPieChart/AccordionWithPieChart.tsx b/src/Frontend/Components/AccordionWithPieChart/AccordionWithPieChart.tsx index fdd6dd86c..b367619cb 100644 --- a/src/Frontend/Components/AccordionWithPieChart/AccordionWithPieChart.tsx +++ b/src/Frontend/Components/AccordionWithPieChart/AccordionWithPieChart.tsx @@ -8,8 +8,6 @@ import MuiAccordionDetails from '@mui/material/AccordionDetails'; import MuiAccordionSummary from '@mui/material/AccordionSummary'; import MuiTypography from '@mui/material/Typography'; -import { text } from '../../../shared/text'; -import { PieChartCriticalityNames } from '../../enums/enums'; import { OpossumColors } from '../../shared-styles'; import { PieChartData } from '../../types/types'; import { PieChart } from '../PieChart/PieChart'; @@ -35,35 +33,18 @@ interface AccordionProps { data: Array; title: string; defaultExpanded?: boolean; + pieChartColorMap?: { [segmentName: string]: string }; } export function getColorsForPieChart( pieChartData: Array, - pieChartTitle: string, + pieChartColorMap?: { [segmentName: string]: string }, ): Array | undefined { - const pieChartColors: Array = []; - - if ( - pieChartTitle === - text.projectStatisticsPopup.charts.criticalSignalsCountPieChart - ) { - for (const pieChartSegment of pieChartData) { - switch (pieChartSegment.name) { - case PieChartCriticalityNames.HighCriticality: - pieChartColors.push(OpossumColors.orange); - break; - case PieChartCriticalityNames.MediumCriticality: - pieChartColors.push(OpossumColors.mediumOrange); - break; - default: - pieChartColors.push(OpossumColors.darkBlue); - break; - } - } - } else { + if (pieChartColorMap === undefined) { return; } - return pieChartColors; + + return pieChartData.map(({ name }) => pieChartColorMap[name]); } export const AccordionWithPieChart: React.FC = (props) => { @@ -86,7 +67,7 @@ export const AccordionWithPieChart: React.FC = (props) => { diff --git a/src/Frontend/Components/AccordionWithPieChart/__tests__/AccordionWithPieChart.test.tsx b/src/Frontend/Components/AccordionWithPieChart/__tests__/AccordionWithPieChart.test.tsx deleted file mode 100644 index cca9f2382..000000000 --- a/src/Frontend/Components/AccordionWithPieChart/__tests__/AccordionWithPieChart.test.tsx +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates -// SPDX-FileCopyrightText: TNG Technology Consulting GmbH -// -// SPDX-License-Identifier: Apache-2.0 -import { text } from '../../../../shared/text'; -import { PieChartCriticalityNames } from '../../../enums/enums'; -import { OpossumColors } from '../../../shared-styles'; -import { PieChartData } from '../../../types/types'; -import { getColorsForPieChart } from '../AccordionWithPieChart'; - -describe('getColorsForPieChart', () => { - it('obtains pie chart colors for critical signals pie chart', () => { - const expectedPieChartColors = [ - OpossumColors.orange, - OpossumColors.mediumOrange, - OpossumColors.darkBlue, - ]; - const criticalSignalsCount: Array = [ - { - name: PieChartCriticalityNames.HighCriticality, - count: 3, - }, - { - name: PieChartCriticalityNames.MediumCriticality, - count: 4, - }, - { - name: PieChartCriticalityNames.NoCriticality, - count: 2, - }, - ]; - - const pieChartColors = getColorsForPieChart( - criticalSignalsCount, - text.projectStatisticsPopup.charts.criticalSignalsCountPieChart, - ); - - expect(pieChartColors).toEqual(expectedPieChartColors); - }); - - it('obtains undefined pie chart colors for default case', () => { - const expectedPieChartColors = undefined; - const sortedMostFrequentLicenses: Array = [ - { - name: 'Apache License Version 2.0', - count: 3, - }, - { - name: 'The MIT License (MIT)', - count: 3, - }, - ]; - - const pieChartColors = getColorsForPieChart( - sortedMostFrequentLicenses, - text.projectStatisticsPopup.charts.mostFrequentLicenseCountPieChart, - ); - - expect(pieChartColors).toEqual(expectedPieChartColors); - }); -}); diff --git a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx index 39385c745..9478d5fe4 100644 --- a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx +++ b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx @@ -6,6 +6,8 @@ import MuiBox from '@mui/material/Box'; import MuiTypography from '@mui/material/Typography'; import { text } from '../../../shared/text'; +import { PieChartCriticalityNames } from '../../enums/enums'; +import { OpossumColors } from '../../shared-styles'; import { closePopup } from '../../state/actions/view-actions/view-actions'; import { useAppDispatch, useAppSelector } from '../../state/hooks'; import { @@ -69,6 +71,12 @@ export const ProjectStatisticsPopup: React.FC = () => { licenseNamesWithCriticality, ); + const criticalSignalsCountColors = { + [PieChartCriticalityNames.HighCriticality]: OpossumColors.orange, + [PieChartCriticalityNames.MediumCriticality]: OpossumColors.mediumOrange, + [PieChartCriticalityNames.NoCriticality]: OpossumColors.darkBlue, + }; + const signalCountByClassification = getSignalCountByClassification( licenseCounts, licenseNamesWithClassification, @@ -84,6 +92,7 @@ export const ProjectStatisticsPopup: React.FC = () => { const isThereAnyPieChartData = mostFrequentLicenseCountData.length > 0 || criticalSignalsCountData.length > 0 || + signalCountByClassification.length > 0 || incompleteAttributionsData.length > 0; function close(): void { @@ -136,6 +145,7 @@ export const ProjectStatisticsPopup: React.FC = () => { text.projectStatisticsPopup.charts .criticalSignalsCountPieChart } + pieChartColorMap={criticalSignalsCountColors} /> criticalityDataWithCount['count'] !== 0, - ); + return criticalityData.filter(({ count }) => count > 0); } export function getSignalCountByClassification( From 31ea3b836475c29878904331d25b0360510b2b94 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Thu, 27 Feb 2025 13:53:22 +0100 Subject: [PATCH 07/24] refactor: remove criticality names enum --- .../ProjectStatisticsPopup.tsx | 33 ++++++++++++++----- .../ProjectStatisticsPopup.util.ts | 9 +++-- .../__tests__/ProjectStatisticsPopup.test.tsx | 6 ++-- .../ProjectStatisticsPopup.util.test.tsx | 9 +++-- src/Frontend/enums/enums.ts | 6 ---- src/shared/text.ts | 16 +++++++-- 6 files changed, 49 insertions(+), 30 deletions(-) diff --git a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx index 9478d5fe4..d92f562ec 100644 --- a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx +++ b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx @@ -5,9 +5,9 @@ import MuiBox from '@mui/material/Box'; import MuiTypography from '@mui/material/Typography'; +import { Criticality } from '../../../shared/shared-types'; import { text } from '../../../shared/text'; -import { PieChartCriticalityNames } from '../../enums/enums'; -import { OpossumColors } from '../../shared-styles'; +import { criticalityColor, OpossumColors } from '../../shared-styles'; import { closePopup } from '../../state/actions/view-actions/view-actions'; import { useAppDispatch, useAppSelector } from '../../state/hooks'; import { @@ -66,15 +66,30 @@ export const ProjectStatisticsPopup: React.FC = () => { const mostFrequentLicenseCountData = getMostFrequentLicenses(licenseCounts); - const criticalSignalsCountData = getCriticalSignalsCount( + const criticalSignalsCount = getCriticalSignalsCount( licenseCounts, licenseNamesWithCriticality, ); + const criticalSignalsCountPieChartData = criticalSignalsCount.map( + ({ criticality, count }) => ({ + name: text.projectStatisticsPopup.charts.criticalSignalsCountPieChart.segmentLabel( + criticality, + ), + count, + }), + ); + const criticalSignalsCountColors = { - [PieChartCriticalityNames.HighCriticality]: OpossumColors.orange, - [PieChartCriticalityNames.MediumCriticality]: OpossumColors.mediumOrange, - [PieChartCriticalityNames.NoCriticality]: OpossumColors.darkBlue, + [text.projectStatisticsPopup.charts.criticalSignalsCountPieChart.segmentLabel( + Criticality.High, + )]: criticalityColor.high, + [text.projectStatisticsPopup.charts.criticalSignalsCountPieChart.segmentLabel( + Criticality.Medium, + )]: criticalityColor.medium, + [text.projectStatisticsPopup.charts.criticalSignalsCountPieChart.segmentLabel( + undefined, + )]: OpossumColors.darkBlue, }; const signalCountByClassification = getSignalCountByClassification( @@ -91,7 +106,7 @@ export const ProjectStatisticsPopup: React.FC = () => { const isThereAnyPieChartData = mostFrequentLicenseCountData.length > 0 || - criticalSignalsCountData.length > 0 || + criticalSignalsCount.length > 0 || signalCountByClassification.length > 0 || incompleteAttributionsData.length > 0; @@ -140,10 +155,10 @@ export const ProjectStatisticsPopup: React.FC = () => { defaultExpanded={true} /> diff --git a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.util.ts b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.util.ts index 3868c9998..db13897f2 100644 --- a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.util.ts +++ b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.util.ts @@ -11,7 +11,6 @@ import { ExternalAttributionSources, PackageInfo, } from '../../../shared/shared-types'; -import { PieChartCriticalityNames } from '../../enums/enums'; import { AttributionCountPerSourcePerLicense, LicenseCounts, @@ -326,7 +325,7 @@ export function getMostFrequentLicenses( export function getCriticalSignalsCount( licenseCounts: LicenseCounts, licenseNamesWithCriticality: LicenseNamesWithCriticality, -): Array { +): Array<{ criticality: Criticality | undefined; count: number }> { const licenseCriticalityCounts = { high: 0, medium: 0, none: 0 }; for (const license of Object.keys( @@ -338,15 +337,15 @@ export function getCriticalSignalsCount( const criticalityData = [ { - name: PieChartCriticalityNames.HighCriticality, + criticality: Criticality.High, count: licenseCriticalityCounts['high'], }, { - name: PieChartCriticalityNames.MediumCriticality, + criticality: Criticality.Medium, count: licenseCriticalityCounts['medium'], }, { - name: PieChartCriticalityNames.NoCriticality, + criticality: undefined, count: licenseCriticalityCounts['none'], }, ]; diff --git a/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx b/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx index f9408d023..acfcae8b8 100644 --- a/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx +++ b/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx @@ -105,7 +105,7 @@ describe('The ProjectStatisticsPopup', () => { ).toBeInTheDocument(); expect( screen.getByText( - text.projectStatisticsPopup.charts.criticalSignalsCountPieChart, + text.projectStatisticsPopup.charts.criticalSignalsCountPieChart.title, ), ).toBeInTheDocument(); expect( @@ -139,7 +139,7 @@ describe('The ProjectStatisticsPopup', () => { ).not.toBeInTheDocument(); expect( screen.queryByText( - text.projectStatisticsPopup.charts.criticalSignalsCountPieChart, + text.projectStatisticsPopup.charts.criticalSignalsCountPieChart.title, ), ).not.toBeInTheDocument(); expect( @@ -192,7 +192,7 @@ describe('The ProjectStatisticsPopup', () => { ).toBeInTheDocument(); expect( screen.getByText( - text.projectStatisticsPopup.charts.criticalSignalsCountPieChart, + text.projectStatisticsPopup.charts.criticalSignalsCountPieChart.title, ), ).toBeInTheDocument(); expect( diff --git a/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.util.test.tsx b/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.util.test.tsx index bac1c2477..5363d5fab 100644 --- a/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.util.test.tsx +++ b/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.util.test.tsx @@ -7,7 +7,6 @@ import { Criticality, ExternalAttributionSources, } from '../../../../shared/shared-types'; -import { PieChartCriticalityNames } from '../../../enums/enums'; import { LicenseCounts, LicenseNamesWithCriticality, @@ -400,17 +399,17 @@ describe('getMostFrequentLicenses', () => { describe('getCriticalSignalsCount', () => { it('counts number of critical signals across all licenses', () => { - const expectedCriticalSignalCount: Array = [ + const expectedCriticalSignalCount = [ { - name: PieChartCriticalityNames.HighCriticality, + criticality: Criticality.High, count: 3, }, { - name: PieChartCriticalityNames.MediumCriticality, + criticality: Criticality.Medium, count: 4, }, { - name: PieChartCriticalityNames.NoCriticality, + criticality: undefined, count: 2, }, ]; diff --git a/src/Frontend/enums/enums.ts b/src/Frontend/enums/enums.ts index 6f702d2e7..e38bf639a 100644 --- a/src/Frontend/enums/enums.ts +++ b/src/Frontend/enums/enums.ts @@ -24,12 +24,6 @@ export enum ButtonText { Delete = 'Delete', } -export enum PieChartCriticalityNames { - HighCriticality = 'Highly critical signals', - MediumCriticality = 'Medium critical signals', - NoCriticality = 'Non-critical signals', -} - export enum AttributionType { FirstParty = 'First Party', ThirdParty = 'Third Party', diff --git a/src/shared/text.ts b/src/shared/text.ts index 984e0af91..162551f1f 100644 --- a/src/shared/text.ts +++ b/src/shared/text.ts @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: TNG Technology Consulting GmbH // // SPDX-License-Identifier: Apache-2.0 -import { FileFormatInfo } from './shared-types'; +import { Criticality, FileFormatInfo } from './shared-types'; function menuLabelForFileFormat(fileFormat: FileFormatInfo): string { return `${fileFormat.name} File (${fileFormat.extensions.map((ext) => `.${ext}`).join('/')})...`; @@ -173,7 +173,19 @@ export const text = { criticalLicensesTable: 'Critical Licenses', pieChartsSectionHeader: 'Pie Charts', mostFrequentLicenseCountPieChart: 'Most Frequent Licenses', - criticalSignalsCountPieChart: 'Signals by Criticality', + criticalSignalsCountPieChart: { + title: 'Signals by Criticality', + segmentLabel: (criticality: Criticality | undefined) => { + switch (criticality) { + case Criticality.High: + return 'Highly Critical Signals'; + case Criticality.Medium: + return 'Medium Critical Signals'; + case undefined: + return 'Non-Critical Signals'; + } + }, + }, signalCountByClassificationPieChart: 'Signals by Classification', incompleteAttributionsPieChart: 'Incomplete Attributions', }, From bcee7c1ca84fd88e8f939027808c72cfeb604e18 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Fri, 28 Feb 2025 09:39:15 +0100 Subject: [PATCH 08/24] refactor: improve usage of text constants --- .../ProjectStatisticsPopup.tsx | 32 +++++++++++-------- .../ProjectStatisticsPopup.util.ts | 4 ++- .../__tests__/ProjectStatisticsPopup.test.tsx | 9 ++++-- src/shared/text.ts | 20 +++++------- 4 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx index d92f562ec..f1d7a161f 100644 --- a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx +++ b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx @@ -71,25 +71,31 @@ export const ProjectStatisticsPopup: React.FC = () => { licenseNamesWithCriticality, ); + const criticalitySegmentLabel = (criticality: Criticality | undefined) => { + switch (criticality) { + case Criticality.High: + return text.projectStatisticsPopup.charts.criticalSignalsCountPieChart + .highlyCritical; + case Criticality.Medium: + return text.projectStatisticsPopup.charts.criticalSignalsCountPieChart + .mediumCritical; + case undefined: + return text.projectStatisticsPopup.charts.criticalSignalsCountPieChart + .nonCritical; + } + }; + const criticalSignalsCountPieChartData = criticalSignalsCount.map( ({ criticality, count }) => ({ - name: text.projectStatisticsPopup.charts.criticalSignalsCountPieChart.segmentLabel( - criticality, - ), + name: criticalitySegmentLabel(criticality), count, }), ); const criticalSignalsCountColors = { - [text.projectStatisticsPopup.charts.criticalSignalsCountPieChart.segmentLabel( - Criticality.High, - )]: criticalityColor.high, - [text.projectStatisticsPopup.charts.criticalSignalsCountPieChart.segmentLabel( - Criticality.Medium, - )]: criticalityColor.medium, - [text.projectStatisticsPopup.charts.criticalSignalsCountPieChart.segmentLabel( - undefined, - )]: OpossumColors.darkBlue, + [criticalitySegmentLabel(Criticality.High)]: criticalityColor.high, + [criticalitySegmentLabel(Criticality.Medium)]: criticalityColor.medium, + [criticalitySegmentLabel(undefined)]: OpossumColors.darkBlue, }; const signalCountByClassification = getSignalCountByClassification( @@ -166,7 +172,7 @@ export const ProjectStatisticsPopup: React.FC = () => { data={signalCountByClassification} title={ text.projectStatisticsPopup.charts - .signalCountByClassificationPieChart + .signalCountByClassificationPieChart.title } /> { ).toBeInTheDocument(); expect( screen.getByText( - text.projectStatisticsPopup.charts.signalCountByClassificationPieChart, + text.projectStatisticsPopup.charts.signalCountByClassificationPieChart + .title, ), ).toBeInTheDocument(); expect( @@ -144,7 +145,8 @@ describe('The ProjectStatisticsPopup', () => { ).not.toBeInTheDocument(); expect( screen.queryByText( - text.projectStatisticsPopup.charts.signalCountByClassificationPieChart, + text.projectStatisticsPopup.charts.signalCountByClassificationPieChart + .title, ), ).not.toBeInTheDocument(); expect( @@ -197,7 +199,8 @@ describe('The ProjectStatisticsPopup', () => { ).toBeInTheDocument(); expect( screen.getByText( - text.projectStatisticsPopup.charts.signalCountByClassificationPieChart, + text.projectStatisticsPopup.charts.signalCountByClassificationPieChart + .title, ), ).toBeInTheDocument(); expect( diff --git a/src/shared/text.ts b/src/shared/text.ts index 162551f1f..c2f108438 100644 --- a/src/shared/text.ts +++ b/src/shared/text.ts @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: TNG Technology Consulting GmbH // // SPDX-License-Identifier: Apache-2.0 -import { Criticality, FileFormatInfo } from './shared-types'; +import { FileFormatInfo } from './shared-types'; function menuLabelForFileFormat(fileFormat: FileFormatInfo): string { return `${fileFormat.name} File (${fileFormat.extensions.map((ext) => `.${ext}`).join('/')})...`; @@ -175,18 +175,14 @@ export const text = { mostFrequentLicenseCountPieChart: 'Most Frequent Licenses', criticalSignalsCountPieChart: { title: 'Signals by Criticality', - segmentLabel: (criticality: Criticality | undefined) => { - switch (criticality) { - case Criticality.High: - return 'Highly Critical Signals'; - case Criticality.Medium: - return 'Medium Critical Signals'; - case undefined: - return 'Non-Critical Signals'; - } - }, + highlyCritical: 'Highly Critical Signals', + mediumCritical: 'Medium Critical Signals', + nonCritical: 'Non-Critical Signals', + }, + signalCountByClassificationPieChart: { + title: 'Signals by Classification', + noClassification: 'No classification', }, - signalCountByClassificationPieChart: 'Signals by Classification', incompleteAttributionsPieChart: 'Incomplete Attributions', }, }, From 79b431431348016b3a8b1b4b55659db1cdb1daa9 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Wed, 26 Feb 2025 15:08:24 +0100 Subject: [PATCH 09/24] feat: delete critical license table --- .../CriticalLicensesTable.tsx | 168 ------------------ .../ProjectStatisticsPopup.tsx | 8 - src/shared/text.ts | 1 - 3 files changed, 177 deletions(-) delete mode 100644 src/Frontend/Components/CriticalLicensesTable/CriticalLicensesTable.tsx diff --git a/src/Frontend/Components/CriticalLicensesTable/CriticalLicensesTable.tsx b/src/Frontend/Components/CriticalLicensesTable/CriticalLicensesTable.tsx deleted file mode 100644 index 43c6f5ee4..000000000 --- a/src/Frontend/Components/CriticalLicensesTable/CriticalLicensesTable.tsx +++ /dev/null @@ -1,168 +0,0 @@ -// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates -// SPDX-FileCopyrightText: TNG Technology Consulting GmbH -// -// SPDX-License-Identifier: Apache-2.0 -import { sortBy } from 'lodash'; - -import { Criticality } from '../../../shared/shared-types'; -import { text } from '../../../shared/text'; -import { clickableIcon } from '../../shared-styles'; -import { LicenseNamesWithCriticality } from '../../types/types'; -import { ProjectLicensesTable } from '../ProjectLicensesTable/ProjectLicensesTable'; - -const LICENSE_COLUMN_NAME_IN_TABLE = 'License name'; -const FOOTER_TITLE = 'Total'; -const TABLE_COLUMN_NAMES = [ - LICENSE_COLUMN_NAME_IN_TABLE, - text.projectStatisticsPopup.criticalLicensesSignalCountColumnName, -]; - -const classes = { - container: { - maxHeight: '400px', - maxWidth: '500px', - marginBottom: '3px', - }, - clickableIcon, - iconButton: { - marginLeft: '8px', - }, -}; - -interface CriticalLicensesTableProps { - totalAttributionsPerLicense: { [licenseName: string]: number }; - licenseNamesWithCriticality: LicenseNamesWithCriticality; - title: string; -} - -interface LicenseNameAndTotalNumberOfAttributions { - licenseName: string; - totalNumberOfAttributions: number; -} - -export const CriticalLicensesTable: React.FC = ( - props, -) => { - const allLicensesWithCriticality = Object.entries( - props.licenseNamesWithCriticality, - ).map((licenseNameAndCriticality) => { - return { - licenseName: licenseNameAndCriticality[0], - criticality: licenseNameAndCriticality[1], - }; - }); - const highCriticalityLicenseNames = getLicenseNamesByCriticality( - allLicensesWithCriticality, - Criticality.High, - ); - const mediumCriticalityLicenseNames = getLicenseNamesByCriticality( - allLicensesWithCriticality, - Criticality.Medium, - ); - const highCriticalityLicensesTotalAttributions: Array = - getCriticalLicenseNamesWithTheirTotalAttributions( - props.totalAttributionsPerLicense, - highCriticalityLicenseNames, - ); - const mediumCriticalityLicensesTotalAttributions: Array = - getCriticalLicenseNamesWithTheirTotalAttributions( - props.totalAttributionsPerLicense, - mediumCriticalityLicenseNames, - ); - const sortedCriticalLicensesTotalAttributions: Array = - sortLicenseNamesWithTotalAttributions( - highCriticalityLicensesTotalAttributions, - ).concat( - sortLicenseNamesWithTotalAttributions( - mediumCriticalityLicensesTotalAttributions, - ), - ); - - return ( - attribution.licenseName, - )} - tableContent={Object.fromEntries( - sortedCriticalLicensesTotalAttributions.map( - ({ licenseName, totalNumberOfAttributions }) => [ - licenseName, - { - [text.projectStatisticsPopup - .criticalLicensesSignalCountColumnName]: - totalNumberOfAttributions, - }, - ], - ), - )} - tableFooter={[FOOTER_TITLE].concat( - getTotalNumberOfAttributions( - sortedCriticalLicensesTotalAttributions, - ).toString(), - )} - licenseNamesWithCriticality={props.licenseNamesWithCriticality} - /> - ); -}; - -function getLicenseNamesByCriticality( - allLicensesWithCriticality: Array<{ - licenseName: string; - criticality: Criticality | undefined; - }>, - criticality: Criticality, -): Array { - return allLicensesWithCriticality - .map((licenseNameAndCriticality) => - licenseNameAndCriticality.criticality === criticality - ? licenseNameAndCriticality.licenseName - : '', - ) - .filter((licenseName) => licenseName !== ''); -} - -function getCriticalLicenseNamesWithTheirTotalAttributions( - totalAttributionsPerLicense: { [licenseName: string]: number }, - criticalLicenseNames: Array, -): Array { - const licenseNamesAndTheirTotalAttributions = criticalLicenseNames.map( - (criticalLicenseName) => { - return { - licenseName: criticalLicenseName, - totalNumberOfAttributions: - totalAttributionsPerLicense[criticalLicenseName], - }; - }, - ); - return licenseNamesAndTheirTotalAttributions.sort(); -} - -function getTotalNumberOfAttributions( - licenseNamesAndTheirTotalAttributions: Array<{ - licenseName: string; - totalNumberOfAttributions: number; - }>, -): number { - return licenseNamesAndTheirTotalAttributions - .map( - (licenseNameAndTotalAttributions) => - licenseNameAndTotalAttributions.totalNumberOfAttributions, - ) - .reduce((total, value) => total + value, 0); -} - -function sortLicenseNamesWithTotalAttributions( - licenseNamesWithTotalAttributions: Array<{ - licenseName: string; - totalNumberOfAttributions: number; - }>, -): Array<{ licenseName: string; totalNumberOfAttributions: number }> { - return sortBy( - licenseNamesWithTotalAttributions, - ({ licenseName }) => licenseName, - ); -} diff --git a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx index f1d7a161f..b10c40daf 100644 --- a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx +++ b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx @@ -21,7 +21,6 @@ import { AccordionWithPieChart } from '../AccordionWithPieChart/AccordionWithPie import { AttributionCountPerSourcePerLicenseTable } from '../AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable'; import { AttributionPropertyCountTable } from '../AttributionPropertyCountTable/AttributionPropertyCountTable'; import { Checkbox } from '../Checkbox/Checkbox'; -import { CriticalLicensesTable } from '../CriticalLicensesTable/CriticalLicensesTable'; import { NotificationPopup } from '../NotificationPopup/NotificationPopup'; import { aggregateAttributionPropertiesFromAttributions, @@ -138,13 +137,6 @@ export const ProjectStatisticsPopup: React.FC = () => { .attributionPropertyCountTable } /> - diff --git a/src/shared/text.ts b/src/shared/text.ts index c2f108438..6152c0ea9 100644 --- a/src/shared/text.ts +++ b/src/shared/text.ts @@ -170,7 +170,6 @@ export const text = { charts: { licenseCountsTable: 'Signals per Sources', attributionPropertyCountTable: 'Attributions Overview', - criticalLicensesTable: 'Critical Licenses', pieChartsSectionHeader: 'Pie Charts', mostFrequentLicenseCountPieChart: 'Most Frequent Licenses', criticalSignalsCountPieChart: { From 663b64889f4b74883434df25a199cfd35b024be9 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Wed, 26 Feb 2025 15:14:01 +0100 Subject: [PATCH 10/24] refactor: delete ProjectLicensesTable component * as there was only one remaining component it, move its functionality there --- ...tributionCountPerSourcePerLicenseTable.tsx | 115 ++++++++++++++++-- .../ProjectLicensesTable.tsx | 107 ---------------- .../ProjectStatisticsPopup.tsx | 1 + 3 files changed, 103 insertions(+), 120 deletions(-) delete mode 100644 src/Frontend/Components/ProjectLicensesTable/ProjectLicensesTable.tsx diff --git a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx index a01816c4e..564189abb 100644 --- a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx +++ b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx @@ -2,10 +2,22 @@ // SPDX-FileCopyrightText: TNG Technology Consulting GmbH // // SPDX-License-Identifier: Apache-2.0 -import { sortBy } from 'lodash'; +import MuiBox from '@mui/material/Box'; +import MuiTable from '@mui/material/Table'; +import MuiTableBody from '@mui/material/TableBody'; +import MuiTableCell from '@mui/material/TableCell'; +import MuiTableContainer from '@mui/material/TableContainer'; +import MuiTableFooter from '@mui/material/TableFooter'; +import MuiTableHead from '@mui/material/TableHead'; +import MuiTableRow from '@mui/material/TableRow'; +import MuiTypography from '@mui/material/Typography'; -import { LicenseCounts, LicenseNamesWithCriticality } from '../../types/types'; -import { ProjectLicensesTable } from '../ProjectLicensesTable/ProjectLicensesTable'; +import { tableClasses } from '../../shared-styles'; +import { + LicenseCounts, + LicenseNamesWithClassification, + LicenseNamesWithCriticality, +} from '../../types/types'; const classes = { container: { @@ -21,6 +33,7 @@ const TOTAL_SOURCES_TITLE = 'Total'; interface AttributionCountPerSourcePerLicenseTableProps { licenseCounts: LicenseCounts; licenseNamesWithCriticality: LicenseNamesWithCriticality; + licenseNamesWithClassification: LicenseNamesWithClassification; title: string; } @@ -58,16 +71,92 @@ export const AttributionCountPerSourcePerLicenseTable: React.FC< props.licenseCounts.totalAttributionsPerLicense[licenseName]), ); + const tableHead = ( + + + {headerRow.map((columnHeader, columnIndex) => ( + + {columnHeader} + + ))} + + + ); + + const buildTableRow = (licenseName: string, rowIndex: number) => { + const licenseNameCell = ( + + {licenseName} + + ); + + const countBySourceCells = sourceNames + .concat(TOTAL_SOURCES_TITLE) + .map((sourceName, sourceIdx) => { + const columnIndex = 1 + sourceIdx; + + return ( + + {props.licenseCounts.attributionCountPerSourcePerLicense[ + licenseName + ][sourceName] || '-'} + + ); + }); + + return ( + + {[licenseNameCell, ...countBySourceCells]} + + ); + }; + + const tableFooter = ( + + + {footerRow.map((total, columnIndex) => ( + + {total} + + ))} + + + ); + return ( - + + {props.title} + + + {tableHead} + + {Object.keys(props.licenseNamesWithCriticality) + .toSorted() + .map(buildTableRow)} + + {tableFooter} + + + ); }; diff --git a/src/Frontend/Components/ProjectLicensesTable/ProjectLicensesTable.tsx b/src/Frontend/Components/ProjectLicensesTable/ProjectLicensesTable.tsx deleted file mode 100644 index d3a30c8a7..000000000 --- a/src/Frontend/Components/ProjectLicensesTable/ProjectLicensesTable.tsx +++ /dev/null @@ -1,107 +0,0 @@ -// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates -// SPDX-FileCopyrightText: TNG Technology Consulting GmbH -// -// SPDX-License-Identifier: Apache-2.0 -import MuiBox from '@mui/material/Box'; -import MuiTable from '@mui/material/Table'; -import MuiTableBody from '@mui/material/TableBody'; -import MuiTableCell from '@mui/material/TableCell'; -import MuiTableContainer from '@mui/material/TableContainer'; -import MuiTableFooter from '@mui/material/TableFooter'; -import MuiTableHead from '@mui/material/TableHead'; -import MuiTableRow from '@mui/material/TableRow'; -import MuiTypography from '@mui/material/Typography'; - -import { Criticality } from '../../../shared/shared-types'; -import { OpossumColors, tableClasses } from '../../shared-styles'; -import { LicenseNamesWithCriticality } from '../../types/types'; - -const PLACEHOLDER_ATTRIBUTION_COUNT = '-'; - -interface TableContent { - [rowName: string]: { [columnName: string]: number }; -} - -interface ProjectLicensesTableProps { - title: string; - containerStyle: { [key: string]: string | number }; - columnHeaders: Array; - columnNames: Array; - rowNames: Array; - tableContent: TableContent; - tableFooter?: Array; - licenseNamesWithCriticality: LicenseNamesWithCriticality; -} - -export const ProjectLicensesTable: React.FC = ( - props, -) => { - return ( - - {props.title} - - - - - {props.columnHeaders.map((columnHeader, columnIndex) => ( - - {columnHeader} - - ))} - - - - {props.rowNames.map((rowName, rowIndex) => ( - - {props.columnNames.map((columnName, columnIndex) => ( - - {columnIndex === 0 ? ( - {rowName} - ) : ( - props.tableContent[rowName][columnName] || - PLACEHOLDER_ATTRIBUTION_COUNT - )} - - ))} - - ))} - - {props.tableFooter && ( - - - {props.tableFooter.map((total, columnIndex) => ( - - {total} - - ))} - - - )} - - - - ); -}; diff --git a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx index b10c40daf..38620dbb4 100644 --- a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx +++ b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx @@ -179,6 +179,7 @@ export const ProjectStatisticsPopup: React.FC = () => { From 6da9bd1276d9ff7704d330c655a6eb63bc5cab76 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Wed, 26 Feb 2025 16:00:41 +0100 Subject: [PATCH 11/24] feat: add criticality column to SignalsPerSources table --- ...tributionCountPerSourcePerLicenseTable.tsx | 51 ++++++++++++++----- .../ProjectStatisticsPopup.tsx | 1 - 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx index 564189abb..2a634c6d8 100644 --- a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx +++ b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx @@ -12,12 +12,9 @@ import MuiTableHead from '@mui/material/TableHead'; import MuiTableRow from '@mui/material/TableRow'; import MuiTypography from '@mui/material/Typography'; -import { tableClasses } from '../../shared-styles'; -import { - LicenseCounts, - LicenseNamesWithClassification, - LicenseNamesWithCriticality, -} from '../../types/types'; +import { Criticality } from '../../../shared/shared-types'; +import { OpossumColors, tableClasses } from '../../shared-styles'; +import { LicenseCounts, LicenseNamesWithCriticality } from '../../types/types'; const classes = { container: { @@ -27,13 +24,13 @@ const classes = { }; const LICENSE_COLUMN_NAME_IN_TABLE = 'License name'; +const CRITICALITY_COLUMN_NAME_IN_TABLE = 'Criticality'; const FOOTER_TITLE = 'Total'; const TOTAL_SOURCES_TITLE = 'Total'; interface AttributionCountPerSourcePerLicenseTableProps { licenseCounts: LicenseCounts; licenseNamesWithCriticality: LicenseNamesWithCriticality; - licenseNamesWithClassification: LicenseNamesWithClassification; title: string; } @@ -54,9 +51,11 @@ export const AttributionCountPerSourcePerLicenseTable: React.FC< .toString(); const footerRow = [FOOTER_TITLE] + .concat('') .concat(totalNumberOfAttributionsPerSource) .concat(totalNumberOfAttributions); const headerRow = [LICENSE_COLUMN_NAME_IN_TABLE] + .concat(CRITICALITY_COLUMN_NAME_IN_TABLE) .concat(sourceNames) .concat(TOTAL_SOURCES_TITLE) .map( @@ -100,10 +99,30 @@ export const AttributionCountPerSourcePerLicenseTable: React.FC< ); - const countBySourceCells = sourceNames - .concat(TOTAL_SOURCES_TITLE) - .map((sourceName, sourceIdx) => { - const columnIndex = 1 + sourceIdx; + const licenseCriticality = props.licenseNamesWithCriticality[licenseName]; + const criticalityColor = + licenseCriticality === Criticality.High + ? OpossumColors.orange + : licenseCriticality === Criticality.Medium + ? OpossumColors.mediumOrange + : undefined; + + const criticalityCell = ( + + {licenseCriticality ?? '-'} + + ); + + const buildCountBySourceCell = + (columnOffset: number) => (sourceName: string, sourceIdx: number) => { + const columnIndex = columnOffset + sourceIdx; return ( ); - }); + }; + + const singleCells = [licenseNameCell, criticalityCell]; return ( - {[licenseNameCell, ...countBySourceCells]} + {singleCells.concat( + sourceNames + .concat(TOTAL_SOURCES_TITLE) + .map(buildCountBySourceCell(singleCells.length)), + )} ); }; diff --git a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx index 38620dbb4..b10c40daf 100644 --- a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx +++ b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx @@ -179,7 +179,6 @@ export const ProjectStatisticsPopup: React.FC = () => { From 36d63c95402d1f631f56ed8a4258e6da87c785be Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Wed, 26 Feb 2025 16:03:21 +0100 Subject: [PATCH 12/24] test: update test for ProjectStatisticsPopup --- .../__tests__/ProjectStatisticsPopup.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx b/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx index 1c9e5e256..fedfeef61 100644 --- a/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx +++ b/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx @@ -221,8 +221,8 @@ describe('The ProjectStatisticsPopup', () => { ), ], }); - expect(screen.getAllByText('License name')).toHaveLength(2); - expect(screen.getAllByText('Total')).toHaveLength(3); + expect(screen.getAllByText('License name')).toHaveLength(1); + expect(screen.getAllByText('Total')).toHaveLength(2); expect(screen.getByText('Follow up')).toBeInTheDocument(); expect(screen.getByText('First party')).toBeInTheDocument(); }); From 8390425236ece7364587c4743708eafc58b3f994 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Wed, 26 Feb 2025 16:35:20 +0100 Subject: [PATCH 13/24] test: fix e2e test for project statistics popup --- .../__tests__/project-statistics.test.ts | 4 ++-- .../page-objects/ProjectStatisticsPopup.ts | 17 +++++------------ 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/e2e-tests/__tests__/project-statistics.test.ts b/src/e2e-tests/__tests__/project-statistics.test.ts index d1f91b578..2aa0cd82d 100644 --- a/src/e2e-tests/__tests__/project-statistics.test.ts +++ b/src/e2e-tests/__tests__/project-statistics.test.ts @@ -54,7 +54,7 @@ test('hidden signals are ignored for project statistics', async ({ await menuBar.openProjectStatistics(); await projectStatisticsPopup.assert.titleIsVisible(); - await projectStatisticsPopup.assert.criticalLicenseCount(2); + await projectStatisticsPopup.assert.totalSignalCount(2); await projectStatisticsPopup.closeButton.click(); await resourcesTree.goto(resourceName1); @@ -67,5 +67,5 @@ test('hidden signals are ignored for project statistics', async ({ await signalsPanel.packageCard.assert.isHidden(packageInfo3); await menuBar.openProjectStatistics(); - await projectStatisticsPopup.assert.criticalLicenseCount(1); + await projectStatisticsPopup.assert.totalSignalCount(1); }); diff --git a/src/e2e-tests/page-objects/ProjectStatisticsPopup.ts b/src/e2e-tests/page-objects/ProjectStatisticsPopup.ts index 624bdfbc7..a1d903a05 100644 --- a/src/e2e-tests/page-objects/ProjectStatisticsPopup.ts +++ b/src/e2e-tests/page-objects/ProjectStatisticsPopup.ts @@ -4,24 +4,19 @@ // SPDX-License-Identifier: Apache-2.0 import { expect, type Locator, type Page } from '@playwright/test'; -import { text } from '../../shared/text'; - export class ProjectStatisticsPopup { private readonly node: Locator; readonly title: Locator; readonly closeButton: Locator; - readonly totalCriticalLicensesCount: Locator; + readonly totalSignalCount: Locator; constructor(window: Page) { this.node = window.getByLabel('project statistics'); this.title = this.node.getByRole('heading').getByText('Project Statistics'); this.closeButton = this.node.getByRole('button', { name: 'Close' }); - const signalsCount = window.getByText( - text.projectStatisticsPopup.criticalLicensesSignalCountColumnName, - ); - this.totalCriticalLicensesCount = this.node + this.totalSignalCount = this.node .getByRole('table') - .filter({ has: signalsCount }) + .filter({ hasText: 'License name' }) .getByRole('row') .last() .getByRole('cell') @@ -35,10 +30,8 @@ export class ProjectStatisticsPopup { titleIsHidden: async (): Promise => { await expect(this.title).toBeHidden(); }, - criticalLicenseCount: async (count: number): Promise => { - await expect(this.totalCriticalLicensesCount).toContainText( - count.toString(), - ); + totalSignalCount: async (count: number): Promise => { + await expect(this.totalSignalCount).toContainText(count.toString()); }, }; } From 864550a94893926971f71998b937cccfa6444cd7 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Wed, 26 Feb 2025 16:51:57 +0100 Subject: [PATCH 14/24] refactor: move user-facing text from signal by source table text.ts --- ...tributionCountPerSourcePerLicenseTable.tsx | 36 ++++++++++--------- .../page-objects/ProjectStatisticsPopup.ts | 7 +++- src/shared/text.ts | 8 +++++ 3 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx index 2a634c6d8..e26569823 100644 --- a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx +++ b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx @@ -13,6 +13,7 @@ import MuiTableRow from '@mui/material/TableRow'; import MuiTypography from '@mui/material/Typography'; import { Criticality } from '../../../shared/shared-types'; +import { text } from '../../../shared/text'; import { OpossumColors, tableClasses } from '../../shared-styles'; import { LicenseCounts, LicenseNamesWithCriticality } from '../../types/types'; @@ -23,10 +24,7 @@ const classes = { }, }; -const LICENSE_COLUMN_NAME_IN_TABLE = 'License name'; -const CRITICALITY_COLUMN_NAME_IN_TABLE = 'Criticality'; -const FOOTER_TITLE = 'Total'; -const TOTAL_SOURCES_TITLE = 'Total'; +const TOTAL_SOURCE_NAME = 'Total'; interface AttributionCountPerSourcePerLicenseTableProps { licenseCounts: LicenseCounts; @@ -50,23 +48,27 @@ export const AttributionCountPerSourcePerLicenseTable: React.FC< .reduce((partialSum, num) => partialSum + num, 0) .toString(); - const footerRow = [FOOTER_TITLE] - .concat('') - .concat(totalNumberOfAttributionsPerSource) - .concat(totalNumberOfAttributions); - const headerRow = [LICENSE_COLUMN_NAME_IN_TABLE] - .concat(CRITICALITY_COLUMN_NAME_IN_TABLE) - .concat(sourceNames) - .concat(TOTAL_SOURCES_TITLE) - .map( - (sourceName) => sourceName.charAt(0).toUpperCase() + sourceName.slice(1), - ); + const footerRow = [ + text.attributionCountPerSourcePerLicenseTable.footerTitle, + '', + ...totalNumberOfAttributionsPerSource, + totalNumberOfAttributions, + ]; + + const headerRow = [ + text.attributionCountPerSourcePerLicenseTable.columnNames.licenseName, + text.attributionCountPerSourcePerLicenseTable.columnNames.criticality, + ...sourceNames, + text.attributionCountPerSourcePerLicenseTable.columnNames.totalSources, + ].map( + (sourceName) => sourceName.charAt(0).toUpperCase() + sourceName.slice(1), + ); Object.entries( props.licenseCounts.attributionCountPerSourcePerLicense, ).forEach( ([licenseName, value]) => - (value[TOTAL_SOURCES_TITLE] = + (value[TOTAL_SOURCE_NAME] = props.licenseCounts.totalAttributionsPerLicense[licenseName]), ); @@ -145,7 +147,7 @@ export const AttributionCountPerSourcePerLicenseTable: React.FC< {singleCells.concat( sourceNames - .concat(TOTAL_SOURCES_TITLE) + .concat(TOTAL_SOURCE_NAME) .map(buildCountBySourceCell(singleCells.length)), )} diff --git a/src/e2e-tests/page-objects/ProjectStatisticsPopup.ts b/src/e2e-tests/page-objects/ProjectStatisticsPopup.ts index a1d903a05..847d68ef5 100644 --- a/src/e2e-tests/page-objects/ProjectStatisticsPopup.ts +++ b/src/e2e-tests/page-objects/ProjectStatisticsPopup.ts @@ -4,6 +4,8 @@ // SPDX-License-Identifier: Apache-2.0 import { expect, type Locator, type Page } from '@playwright/test'; +import { text } from '../../shared/text'; + export class ProjectStatisticsPopup { private readonly node: Locator; readonly title: Locator; @@ -16,7 +18,10 @@ export class ProjectStatisticsPopup { this.closeButton = this.node.getByRole('button', { name: 'Close' }); this.totalSignalCount = this.node .getByRole('table') - .filter({ hasText: 'License name' }) + .filter({ + hasText: + text.attributionCountPerSourcePerLicenseTable.columnNames.licenseName, + }) .getByRole('row') .last() .getByRole('cell') diff --git a/src/shared/text.ts b/src/shared/text.ts index 6152c0ea9..e16864a5d 100644 --- a/src/shared/text.ts +++ b/src/shared/text.ts @@ -185,6 +185,14 @@ export const text = { incompleteAttributionsPieChart: 'Incomplete Attributions', }, }, + attributionCountPerSourcePerLicenseTable: { + footerTitle: 'Total', + columnNames: { + licenseName: 'License Name', + criticality: 'Criticality', + totalSources: 'Total', + }, + }, unsavedChangesPopup: { title: 'Unsaved Changes', message: 'You have unsaved changes. What would you like to do?', From 4ddb70918c406c48b0e5cf0a06897c500393bcca Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Wed, 26 Feb 2025 16:56:51 +0100 Subject: [PATCH 15/24] test: fix failing unit test for project statistics popup --- .../__tests__/ProjectStatisticsPopup.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx b/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx index fedfeef61..b2585084c 100644 --- a/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx +++ b/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx @@ -221,7 +221,7 @@ describe('The ProjectStatisticsPopup', () => { ), ], }); - expect(screen.getAllByText('License name')).toHaveLength(1); + expect(screen.getAllByText('License Name')).toHaveLength(1); expect(screen.getAllByText('Total')).toHaveLength(2); expect(screen.getByText('Follow up')).toBeInTheDocument(); expect(screen.getByText('First party')).toBeInTheDocument(); From f3313d679cf8c33d7856980355651f39223e475b Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Thu, 27 Feb 2025 17:02:24 +0100 Subject: [PATCH 16/24] feat: change criticality display to use icons instead of plain text in signals per sources table --- ...tributionCountPerSourcePerLicenseTable.tsx | 29 ++++++++++++------- src/shared/text.ts | 6 +++- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx index e26569823..9fe45c985 100644 --- a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx +++ b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx @@ -14,8 +14,9 @@ import MuiTypography from '@mui/material/Typography'; import { Criticality } from '../../../shared/shared-types'; import { text } from '../../../shared/text'; -import { OpossumColors, tableClasses } from '../../shared-styles'; +import { tableClasses } from '../../shared-styles'; import { LicenseCounts, LicenseNamesWithCriticality } from '../../types/types'; +import { CriticalityIcon } from '../Icons/Icons'; const classes = { container: { @@ -57,7 +58,7 @@ export const AttributionCountPerSourcePerLicenseTable: React.FC< const headerRow = [ text.attributionCountPerSourcePerLicenseTable.columnNames.licenseName, - text.attributionCountPerSourcePerLicenseTable.columnNames.criticality, + text.attributionCountPerSourcePerLicenseTable.columnNames.criticality.title, ...sourceNames, text.attributionCountPerSourcePerLicenseTable.columnNames.totalSources, ].map( @@ -102,23 +103,31 @@ export const AttributionCountPerSourcePerLicenseTable: React.FC< ); const licenseCriticality = props.licenseNamesWithCriticality[licenseName]; - const criticalityColor = - licenseCriticality === Criticality.High - ? OpossumColors.orange - : licenseCriticality === Criticality.Medium - ? OpossumColors.mediumOrange - : undefined; const criticalityCell = ( - {licenseCriticality ?? '-'} + + {licenseCriticality === undefined ? ( + '-' + ) : ( + + )} + ); diff --git a/src/shared/text.ts b/src/shared/text.ts index e16864a5d..20f19d4f7 100644 --- a/src/shared/text.ts +++ b/src/shared/text.ts @@ -189,7 +189,11 @@ export const text = { footerTitle: 'Total', columnNames: { licenseName: 'License Name', - criticality: 'Criticality', + criticality: { + title: 'Criticality', + medium: 'Medium Criticality', + high: 'High Criticality', + }, totalSources: 'Total', }, }, From 5030182dfcb9ef1defdccf546893d9792281b0e5 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Fri, 28 Feb 2025 11:26:00 +0100 Subject: [PATCH 17/24] refactor: introduce subcomponents for signal per source table --- ...tributionCountPerSourcePerLicenseTable.tsx | 182 +++--------------- ...ionCountPerSourcePerLicenseTableFooter.tsx | 43 +++++ ...utionCountPerSourcePerLicenseTableHead.tsx | 45 +++++ ...butionCountPerSourcePerLicenseTableRow.tsx | 67 +++++++ 4 files changed, 185 insertions(+), 152 deletions(-) create mode 100644 src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableFooter/AttributionCountPerSourcePerLicenseTableFooter.tsx create mode 100644 src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableHead/AttributionCountPerSourcePerLicenseTableHead.tsx create mode 100644 src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableRow/AttributionCountPerSourcePerLicenseTableRow.tsx diff --git a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx index 9fe45c985..07097f914 100644 --- a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx +++ b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx @@ -5,18 +5,13 @@ import MuiBox from '@mui/material/Box'; import MuiTable from '@mui/material/Table'; import MuiTableBody from '@mui/material/TableBody'; -import MuiTableCell from '@mui/material/TableCell'; import MuiTableContainer from '@mui/material/TableContainer'; -import MuiTableFooter from '@mui/material/TableFooter'; -import MuiTableHead from '@mui/material/TableHead'; -import MuiTableRow from '@mui/material/TableRow'; import MuiTypography from '@mui/material/Typography'; -import { Criticality } from '../../../shared/shared-types'; -import { text } from '../../../shared/text'; -import { tableClasses } from '../../shared-styles'; import { LicenseCounts, LicenseNamesWithCriticality } from '../../types/types'; -import { CriticalityIcon } from '../Icons/Icons'; +import { AttributionCountPerSourcePerLicenseTableFooter } from './AttributionCountPerSourcePerLicenseTableFooter/AttributionCountPerSourcePerLicenseTableFooter'; +import { AttributionCountPerSourcePerLicenseTableHead } from './AttributionCountPerSourcePerLicenseTableHead/AttributionCountPerSourcePerLicenseTableHead'; +import { AttributionCountPerSourcePerLicenseTableRow } from './AttributionCountPerSourcePerLicenseTableRow/AttributionCountPerSourcePerLicenseTableRow'; const classes = { container: { @@ -25,8 +20,6 @@ const classes = { }, }; -const TOTAL_SOURCE_NAME = 'Total'; - interface AttributionCountPerSourcePerLicenseTableProps { licenseCounts: LicenseCounts; licenseNamesWithCriticality: LicenseNamesWithCriticality; @@ -40,157 +33,42 @@ export const AttributionCountPerSourcePerLicenseTable: React.FC< props.licenseCounts.totalAttributionsPerSource, ); - const totalNumberOfAttributionsPerSource = sourceNames.map((sourceName) => - props.licenseCounts.totalAttributionsPerSource[sourceName].toString(), - ); - const totalNumberOfAttributions = Object.values( - props.licenseCounts.totalAttributionsPerSource, - ) - .reduce((partialSum, num) => partialSum + num, 0) - .toString(); - - const footerRow = [ - text.attributionCountPerSourcePerLicenseTable.footerTitle, - '', - ...totalNumberOfAttributionsPerSource, - totalNumberOfAttributions, - ]; - - const headerRow = [ - text.attributionCountPerSourcePerLicenseTable.columnNames.licenseName, - text.attributionCountPerSourcePerLicenseTable.columnNames.criticality.title, - ...sourceNames, - text.attributionCountPerSourcePerLicenseTable.columnNames.totalSources, - ].map( - (sourceName) => sourceName.charAt(0).toUpperCase() + sourceName.slice(1), - ); - - Object.entries( - props.licenseCounts.attributionCountPerSourcePerLicense, - ).forEach( - ([licenseName, value]) => - (value[TOTAL_SOURCE_NAME] = - props.licenseCounts.totalAttributionsPerLicense[licenseName]), - ); - - const tableHead = ( - - - {headerRow.map((columnHeader, columnIndex) => ( - - {columnHeader} - - ))} - - - ); - - const buildTableRow = (licenseName: string, rowIndex: number) => { - const licenseNameCell = ( - - {licenseName} - - ); - - const licenseCriticality = props.licenseNamesWithCriticality[licenseName]; - - const criticalityCell = ( - - - {licenseCriticality === undefined ? ( - '-' - ) : ( - - )} - - - ); - - const buildCountBySourceCell = - (columnOffset: number) => (sourceName: string, sourceIdx: number) => { - const columnIndex = columnOffset + sourceIdx; - - return ( - - {props.licenseCounts.attributionCountPerSourcePerLicense[ - licenseName - ][sourceName] || '-'} - - ); - }; - - const singleCells = [licenseNameCell, criticalityCell]; - - return ( - - {singleCells.concat( - sourceNames - .concat(TOTAL_SOURCE_NAME) - .map(buildCountBySourceCell(singleCells.length)), - )} - - ); - }; - - const tableFooter = ( - - - {footerRow.map((total, columnIndex) => ( - - {total} - - ))} - - - ); - return ( {props.title} - {tableHead} + {Object.keys(props.licenseNamesWithCriticality) .toSorted() - .map(buildTableRow)} + .map((licenseName, rowIndex) => ( + + ))} - {tableFooter} + diff --git a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableFooter/AttributionCountPerSourcePerLicenseTableFooter.tsx b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableFooter/AttributionCountPerSourcePerLicenseTableFooter.tsx new file mode 100644 index 000000000..0d1c0e884 --- /dev/null +++ b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableFooter/AttributionCountPerSourcePerLicenseTableFooter.tsx @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 +import MuiTableCell from '@mui/material/TableCell'; +import MuiTableFooter from '@mui/material/TableFooter'; +import MuiTableRow from '@mui/material/TableRow'; +import _ from 'lodash'; + +import { text } from '../../../../shared/text'; +import { tableClasses } from '../../../shared-styles'; + +interface AttributionCountPerSourcePerLicenseTableFooterProps { + sourceNames: Array; + totalAttributionsPerSource: { [sourceName: string]: number }; +} + +export const AttributionCountPerSourcePerLicenseTableFooter: React.FC< + AttributionCountPerSourcePerLicenseTableFooterProps +> = (props) => { + return ( + + + + {text.attributionCountPerSourcePerLicenseTable.footerTitle} + + + {props.sourceNames.map((sourceName, sourceIdx) => ( + + {props.totalAttributionsPerSource[sourceName]} + + ))} + + {_.sum(Object.values(props.totalAttributionsPerSource))} + + + + ); +}; diff --git a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableHead/AttributionCountPerSourcePerLicenseTableHead.tsx b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableHead/AttributionCountPerSourcePerLicenseTableHead.tsx new file mode 100644 index 000000000..755e08831 --- /dev/null +++ b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableHead/AttributionCountPerSourcePerLicenseTableHead.tsx @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 +import MuiTableCell from '@mui/material/TableCell'; +import MuiTableHead from '@mui/material/TableHead'; +import MuiTableRow from '@mui/material/TableRow'; + +import { text } from '../../../../shared/text'; +import { tableClasses } from '../../../shared-styles'; + +interface AttributionCountPerSourcePerLicenseTableHeadProps { + sourceNames: Array; +} + +export const AttributionCountPerSourcePerLicenseTableHead: React.FC< + AttributionCountPerSourcePerLicenseTableHeadProps +> = (props) => { + const componentText = text.attributionCountPerSourcePerLicenseTable; + + const headerRow = [ + componentText.columnNames.licenseName, + componentText.columnNames.criticality.title, + ...props.sourceNames, + componentText.columnNames.totalSources, + ].map( + (sourceName) => sourceName.charAt(0).toUpperCase() + sourceName.slice(1), + ); + + return ( + + + {headerRow.map((columnHeader, columnIndex) => ( + + {columnHeader} + + ))} + + + ); +}; diff --git a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableRow/AttributionCountPerSourcePerLicenseTableRow.tsx b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableRow/AttributionCountPerSourcePerLicenseTableRow.tsx new file mode 100644 index 000000000..4cf47a2fb --- /dev/null +++ b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableRow/AttributionCountPerSourcePerLicenseTableRow.tsx @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 +import MuiTableCell from '@mui/material/TableCell'; +import MuiTableRow from '@mui/material/TableRow'; + +import { Criticality } from '../../../../shared/shared-types'; +import { text } from '../../../../shared/text'; +import { tableClasses } from '../../../shared-styles'; +import { CriticalityIcon } from '../../Icons/Icons'; + +interface AttributionCountPerSourcePerLicenseTableRowProps { + sourceNames: Array; + signalCountsPerSource: { [sourceName: string]: number }; + licenseName: string; + licenseCriticality: Criticality | undefined; + totalSignalCount: number; + key: React.Key; +} + +export const AttributionCountPerSourcePerLicenseTableRow: React.FC< + AttributionCountPerSourcePerLicenseTableRowProps +> = (props) => { + return ( + + + {props.licenseName} + + {renderCriticalityCell()} + {props.sourceNames.map((sourceName, sourceIdx) => ( + + {props.signalCountsPerSource[sourceName] || '-'} + + ))} + + {props.totalSignalCount} + + + ); + + function renderCriticalityCell() { + return ( + + {props.licenseCriticality === undefined ? ( + '-' + ) : ( + + )} + + ); + } +}; From d5d63551f4e3c67e299329f94b21436e224e8b83 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Wed, 26 Feb 2025 17:48:47 +0100 Subject: [PATCH 18/24] feat: add classification column to signals per sources table in project statistics popup --- .../AttributionCountPerSourcePerLicenseTable.tsx | 7 ++++++- .../ProjectStatisticsPopup/ProjectStatisticsPopup.tsx | 1 + src/shared/text.ts | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx index 07097f914..d404ab951 100644 --- a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx +++ b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx @@ -8,7 +8,11 @@ import MuiTableBody from '@mui/material/TableBody'; import MuiTableContainer from '@mui/material/TableContainer'; import MuiTypography from '@mui/material/Typography'; -import { LicenseCounts, LicenseNamesWithCriticality } from '../../types/types'; +import { + LicenseCounts, + LicenseNamesWithClassification, + LicenseNamesWithCriticality, +} from '../../types/types'; import { AttributionCountPerSourcePerLicenseTableFooter } from './AttributionCountPerSourcePerLicenseTableFooter/AttributionCountPerSourcePerLicenseTableFooter'; import { AttributionCountPerSourcePerLicenseTableHead } from './AttributionCountPerSourcePerLicenseTableHead/AttributionCountPerSourcePerLicenseTableHead'; import { AttributionCountPerSourcePerLicenseTableRow } from './AttributionCountPerSourcePerLicenseTableRow/AttributionCountPerSourcePerLicenseTableRow'; @@ -23,6 +27,7 @@ const classes = { interface AttributionCountPerSourcePerLicenseTableProps { licenseCounts: LicenseCounts; licenseNamesWithCriticality: LicenseNamesWithCriticality; + licenseNamesWithClassification: LicenseNamesWithClassification; title: string; } diff --git a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx index b10c40daf..38620dbb4 100644 --- a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx +++ b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx @@ -179,6 +179,7 @@ export const ProjectStatisticsPopup: React.FC = () => { diff --git a/src/shared/text.ts b/src/shared/text.ts index 20f19d4f7..3546b1347 100644 --- a/src/shared/text.ts +++ b/src/shared/text.ts @@ -194,6 +194,7 @@ export const text = { medium: 'Medium Criticality', high: 'High Criticality', }, + classification: 'Classification', totalSources: 'Total', }, }, From cff656db6dae5c62acf93a86b67f5d22c050ba10 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Thu, 27 Feb 2025 17:12:06 +0100 Subject: [PATCH 19/24] feat: improve styling of signals per sources table for better readability * group columns into license info and source count columns * give alternating background color to table rows --- src/e2e-tests/page-objects/ProjectStatisticsPopup.ts | 2 +- src/shared/text.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/e2e-tests/page-objects/ProjectStatisticsPopup.ts b/src/e2e-tests/page-objects/ProjectStatisticsPopup.ts index 847d68ef5..3bd6c421a 100644 --- a/src/e2e-tests/page-objects/ProjectStatisticsPopup.ts +++ b/src/e2e-tests/page-objects/ProjectStatisticsPopup.ts @@ -20,7 +20,7 @@ export class ProjectStatisticsPopup { .getByRole('table') .filter({ hasText: - text.attributionCountPerSourcePerLicenseTable.columnNames.licenseName, + text.attributionCountPerSourcePerLicenseTable.columns.licenseName, }) .getByRole('row') .last() diff --git a/src/shared/text.ts b/src/shared/text.ts index 3546b1347..2cc01c15f 100644 --- a/src/shared/text.ts +++ b/src/shared/text.ts @@ -187,8 +187,10 @@ export const text = { }, attributionCountPerSourcePerLicenseTable: { footerTitle: 'Total', - columnNames: { - licenseName: 'License Name', + columns: { + licenseInfo: 'License Info', + signalCountPerSource: 'Signal Count per Source', + licenseName: 'Name', criticality: { title: 'Criticality', medium: 'Medium Criticality', @@ -197,6 +199,7 @@ export const text = { classification: 'Classification', totalSources: 'Total', }, + absent: '-', }, unsavedChangesPopup: { title: 'Unsaved Changes', From a93da94e8aa2af84454aa655c2b977638add91ca Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Thu, 27 Feb 2025 17:16:40 +0100 Subject: [PATCH 20/24] test: fix failing unit test --- .../__tests__/ProjectStatisticsPopup.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx b/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx index b2585084c..f0661c16f 100644 --- a/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx +++ b/src/Frontend/Components/ProjectStatisticsPopup/__tests__/ProjectStatisticsPopup.test.tsx @@ -221,7 +221,7 @@ describe('The ProjectStatisticsPopup', () => { ), ], }); - expect(screen.getAllByText('License Name')).toHaveLength(1); + expect(screen.getAllByText('License Info')).toHaveLength(1); expect(screen.getAllByText('Total')).toHaveLength(2); expect(screen.getByText('Follow up')).toBeInTheDocument(); expect(screen.getByText('First party')).toBeInTheDocument(); From 3ec527845d52a030887837ba4e8257858a69af14 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Fri, 28 Feb 2025 11:53:10 +0100 Subject: [PATCH 21/24] chore: post-rebase cleanup --- ...tributionCountPerSourcePerLicenseTable.tsx | 4 ++ ...ionCountPerSourcePerLicenseTableFooter.tsx | 1 + ...utionCountPerSourcePerLicenseTableHead.tsx | 56 ++++++++++++++++--- ...butionCountPerSourcePerLicenseTableRow.tsx | 55 +++++++++++++----- 4 files changed, 93 insertions(+), 23 deletions(-) diff --git a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx index d404ab951..f0425c8f2 100644 --- a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx +++ b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx @@ -61,10 +61,14 @@ export const AttributionCountPerSourcePerLicenseTable: React.FC< licenseCriticality={ props.licenseNamesWithCriticality[licenseName] } + licenseClassification={ + props.licenseNamesWithClassification[licenseName] + } totalSignalCount={ props.licenseCounts.totalAttributionsPerLicense[licenseName] } key={rowIndex} + rowIndex={rowIndex} /> ))} diff --git a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableFooter/AttributionCountPerSourcePerLicenseTableFooter.tsx b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableFooter/AttributionCountPerSourcePerLicenseTableFooter.tsx index 0d1c0e884..425af5811 100644 --- a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableFooter/AttributionCountPerSourcePerLicenseTableFooter.tsx +++ b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableFooter/AttributionCountPerSourcePerLicenseTableFooter.tsx @@ -25,6 +25,7 @@ export const AttributionCountPerSourcePerLicenseTableFooter: React.FC< {text.attributionCountPerSourcePerLicenseTable.footerTitle} + {props.sourceNames.map((sourceName, sourceIdx) => ( ; } @@ -19,20 +28,49 @@ export const AttributionCountPerSourcePerLicenseTableHead: React.FC< const componentText = text.attributionCountPerSourcePerLicenseTable; const headerRow = [ - componentText.columnNames.licenseName, - componentText.columnNames.criticality.title, - ...props.sourceNames, - componentText.columnNames.totalSources, - ].map( - (sourceName) => sourceName.charAt(0).toUpperCase() + sourceName.slice(1), - ); + componentText.columns.licenseName, + componentText.columns.criticality.title, + componentText.columns.classification, + ...props.sourceNames.map( + (sourceName) => sourceName.charAt(0).toUpperCase() + sourceName.slice(1), + ), + componentText.columns.totalSources, + ]; return ( - + + + + {componentText.columns.licenseInfo} + + + {componentText.columns.signalCountPerSource} + + {headerRow.map((columnHeader, columnIndex) => ( diff --git a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableRow/AttributionCountPerSourcePerLicenseTableRow.tsx b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableRow/AttributionCountPerSourcePerLicenseTableRow.tsx index 4cf47a2fb..9f79adb41 100644 --- a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableRow/AttributionCountPerSourcePerLicenseTableRow.tsx +++ b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableRow/AttributionCountPerSourcePerLicenseTableRow.tsx @@ -7,7 +7,9 @@ import MuiTableRow from '@mui/material/TableRow'; import { Criticality } from '../../../../shared/shared-types'; import { text } from '../../../../shared/text'; -import { tableClasses } from '../../../shared-styles'; +import { OpossumColors, tableClasses } from '../../../shared-styles'; +import { useAppSelector } from '../../../state/hooks'; +import { getClassifications } from '../../../state/selectors/resource-selectors'; import { CriticalityIcon } from '../../Icons/Icons'; interface AttributionCountPerSourcePerLicenseTableRowProps { @@ -15,25 +17,44 @@ interface AttributionCountPerSourcePerLicenseTableRowProps { signalCountsPerSource: { [sourceName: string]: number }; licenseName: string; licenseCriticality: Criticality | undefined; + licenseClassification: number | undefined; totalSignalCount: number; key: React.Key; + rowIndex: number; } export const AttributionCountPerSourcePerLicenseTableRow: React.FC< AttributionCountPerSourcePerLicenseTableRowProps > = (props) => { + const bodyClassWithBackgroundColor = { + ...tableClasses.body, + backgroundColor: + props.rowIndex % 2 === 0 + ? OpossumColors.lightestBlue + : OpossumColors.almostWhiteBlue, + }; + + const componentText = text.attributionCountPerSourcePerLicenseTable; + + const classifications = useAppSelector(getClassifications); + return ( - + {props.licenseName} {renderCriticalityCell()} + {renderClassificationCell()} {props.sourceNames.map((sourceName, sourceIdx) => ( - + {props.signalCountsPerSource[sourceName] || '-'} ))} - + {props.totalSignalCount} @@ -41,12 +62,7 @@ export const AttributionCountPerSourcePerLicenseTableRow: React.FC< function renderCriticalityCell() { return ( - + {props.licenseCriticality === undefined ? ( '-' ) : ( @@ -54,14 +70,25 @@ export const AttributionCountPerSourcePerLicenseTableRow: React.FC< criticality={props.licenseCriticality} tooltip={ props.licenseCriticality === Criticality.High - ? text.attributionCountPerSourcePerLicenseTable.columnNames - .criticality.high - : text.attributionCountPerSourcePerLicenseTable.columnNames - .criticality.medium + ? componentText.columns.criticality.high + : componentText.columns.criticality.medium } /> )} ); } + + function renderClassificationCell() { + return ( + + + {props.licenseClassification + ? (classifications[props.licenseClassification] ?? + componentText.absent) + : componentText.absent} + + + ); + } }; From 23ac1dbd6bee05dda4bd7a0392643537f1456c8b Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Fri, 28 Feb 2025 11:55:31 +0100 Subject: [PATCH 22/24] refactor: improve use of text constants --- .../AttributionCountPerSourcePerLicenseTableRow.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableRow/AttributionCountPerSourcePerLicenseTableRow.tsx b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableRow/AttributionCountPerSourcePerLicenseTableRow.tsx index 9f79adb41..0a1cdcabc 100644 --- a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableRow/AttributionCountPerSourcePerLicenseTableRow.tsx +++ b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableRow/AttributionCountPerSourcePerLicenseTableRow.tsx @@ -51,7 +51,7 @@ export const AttributionCountPerSourcePerLicenseTableRow: React.FC< align={'center'} key={sourceIdx} > - {props.signalCountsPerSource[sourceName] || '-'} + {props.signalCountsPerSource[sourceName] || componentText.absent} ))} @@ -64,7 +64,7 @@ export const AttributionCountPerSourcePerLicenseTableRow: React.FC< return ( {props.licenseCriticality === undefined ? ( - '-' + componentText.absent ) : ( Date: Fri, 28 Feb 2025 13:46:00 +0100 Subject: [PATCH 23/24] refactor: text constant name change --- .../AttributionCountPerSourcePerLicenseTableRow.tsx | 8 ++++---- src/shared/text.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableRow/AttributionCountPerSourcePerLicenseTableRow.tsx b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableRow/AttributionCountPerSourcePerLicenseTableRow.tsx index 0a1cdcabc..d3057ae3f 100644 --- a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableRow/AttributionCountPerSourcePerLicenseTableRow.tsx +++ b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableRow/AttributionCountPerSourcePerLicenseTableRow.tsx @@ -51,7 +51,7 @@ export const AttributionCountPerSourcePerLicenseTableRow: React.FC< align={'center'} key={sourceIdx} > - {props.signalCountsPerSource[sourceName] || componentText.absent} + {props.signalCountsPerSource[sourceName] || componentText.none} ))} @@ -64,7 +64,7 @@ export const AttributionCountPerSourcePerLicenseTableRow: React.FC< return ( {props.licenseCriticality === undefined ? ( - componentText.absent + componentText.none ) : ( {props.licenseClassification ? (classifications[props.licenseClassification] ?? - componentText.absent) - : componentText.absent} + componentText.none) + : componentText.none} ); diff --git a/src/shared/text.ts b/src/shared/text.ts index 2cc01c15f..bd85400b6 100644 --- a/src/shared/text.ts +++ b/src/shared/text.ts @@ -199,7 +199,7 @@ export const text = { classification: 'Classification', totalSources: 'Total', }, - absent: '-', + none: '-', }, unsavedChangesPopup: { title: 'Unsaved Changes', From 03ade535512f39dec7de9abcedf036305777dd54 Mon Sep 17 00:00:00 2001 From: Adrian Braemer Date: Fri, 28 Feb 2025 14:06:31 +0100 Subject: [PATCH 24/24] refactor: introduce simplified statistics code and use it for pie-charts --- .../ProjectStatisticsPopup.tsx | 93 ++++++++++--------- .../ProjectStatisticsPopup.util.ts | 88 +++++++++++++++++- 2 files changed, 137 insertions(+), 44 deletions(-) diff --git a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx index 38620dbb4..ebf952fa7 100644 --- a/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx +++ b/src/Frontend/Components/ProjectStatisticsPopup/ProjectStatisticsPopup.tsx @@ -4,6 +4,7 @@ // SPDX-License-Identifier: Apache-2.0 import MuiBox from '@mui/material/Box'; import MuiTypography from '@mui/material/Typography'; +import { countBy } from 'lodash'; import { Criticality } from '../../../shared/shared-types'; import { text } from '../../../shared/text'; @@ -25,11 +26,10 @@ import { NotificationPopup } from '../NotificationPopup/NotificationPopup'; import { aggregateAttributionPropertiesFromAttributions, aggregateLicensesAndSourcesFromAttributions, - getCriticalSignalsCount, - getIncompleteAttributionsCount, - getMostFrequentLicenses, - getSignalCountByClassification, + convertToPieChartData, + getMostFrequentLicenses2, getUniqueLicenseNameToAttribution, + prepareStatistics, } from './ProjectStatisticsPopup.util'; const classes = { @@ -38,6 +38,22 @@ const classes = { rightPanel: { flexGrow: 1, marginLeft: '2vw' }, }; +const MAX_DISPLAYED_FREQUENT_LICENSES = 5; + +const criticalitySegmentLabel = (criticality: Criticality | undefined) => { + switch (criticality) { + case Criticality.High: + return text.projectStatisticsPopup.charts.criticalSignalsCountPieChart + .highlyCritical; + case Criticality.Medium: + return text.projectStatisticsPopup.charts.criticalSignalsCountPieChart + .mediumCritical; + case undefined: + return text.projectStatisticsPopup.charts.criticalSignalsCountPieChart + .nonCritical; + } +}; + export const ProjectStatisticsPopup: React.FC = () => { const dispatch = useAppDispatch(); @@ -49,6 +65,34 @@ export const ProjectStatisticsPopup: React.FC = () => { getUnresolvedExternalAttributions, ); + const externalAttributionStats = prepareStatistics( + unresolvedExternalAttribution, + attributionSources, + ); + const mostFrequentLicenseCountData = getMostFrequentLicenses2( + externalAttributionStats, + MAX_DISPLAYED_FREQUENT_LICENSES, + ); + const criticalSignalsCountData = convertToPieChartData( + countBy(externalAttributionStats, ({ criticality }) => + criticalitySegmentLabel(criticality), + ), + ); + const signalCountByClassification = convertToPieChartData( + countBy( + externalAttributionStats, + ({ classification }) => + classifications[classification] ?? 'No Classification', + ), + ); + + const manualAttributionStats = prepareStatistics(manualAttributions); + const incompleteAttributionsData = convertToPieChartData( + countBy(manualAttributionStats, ({ isIncomplete }) => + isIncomplete ? 'Incomplete attributions' : 'Complete attributions', + ), + ); + const strippedLicenseNameToAttribution = getUniqueLicenseNameToAttribution( unresolvedExternalAttribution, ); @@ -63,55 +107,18 @@ export const ProjectStatisticsPopup: React.FC = () => { attributionSources, ); - const mostFrequentLicenseCountData = getMostFrequentLicenses(licenseCounts); - - const criticalSignalsCount = getCriticalSignalsCount( - licenseCounts, - licenseNamesWithCriticality, - ); - - const criticalitySegmentLabel = (criticality: Criticality | undefined) => { - switch (criticality) { - case Criticality.High: - return text.projectStatisticsPopup.charts.criticalSignalsCountPieChart - .highlyCritical; - case Criticality.Medium: - return text.projectStatisticsPopup.charts.criticalSignalsCountPieChart - .mediumCritical; - case undefined: - return text.projectStatisticsPopup.charts.criticalSignalsCountPieChart - .nonCritical; - } - }; - - const criticalSignalsCountPieChartData = criticalSignalsCount.map( - ({ criticality, count }) => ({ - name: criticalitySegmentLabel(criticality), - count, - }), - ); - const criticalSignalsCountColors = { [criticalitySegmentLabel(Criticality.High)]: criticalityColor.high, [criticalitySegmentLabel(Criticality.Medium)]: criticalityColor.medium, [criticalitySegmentLabel(undefined)]: OpossumColors.darkBlue, }; - const signalCountByClassification = getSignalCountByClassification( - licenseCounts, - licenseNamesWithClassification, - classifications, - ); - const manualAttributionPropertyCounts = aggregateAttributionPropertiesFromAttributions(manualAttributions); - const incompleteAttributionsData = - getIncompleteAttributionsCount(manualAttributions); - const isThereAnyPieChartData = mostFrequentLicenseCountData.length > 0 || - criticalSignalsCount.length > 0 || + criticalSignalsCountData.length > 0 || signalCountByClassification.length > 0 || incompleteAttributionsData.length > 0; @@ -153,7 +160,7 @@ export const ProjectStatisticsPopup: React.FC = () => { defaultExpanded={true} /> // // SPDX-License-Identifier: Apache-2.0 -import { pickBy, toNumber } from 'lodash'; +import { Dictionary, groupBy, maxBy, pickBy, toNumber } from 'lodash'; import { Attributions, @@ -34,6 +34,92 @@ const UNKNOWN_SOURCE_PLACEHOLDER = '-'; // exported only for tests export const ATTRIBUTION_TOTAL = 'Total Attributions'; +export interface AttributionStatistics { + licenseName: string; + criticality: Criticality | undefined; + classification: number; + sourceName?: string; + isIncomplete: boolean; + needsReview: boolean; + followUp: boolean; + firstParty: boolean; +} + +function getSourceName( + package_info: PackageInfo, + attributionSources: ExternalAttributionSources, +) { + const sourceId = package_info.source?.name ?? UNKNOWN_SOURCE_PLACEHOLDER; + return Object.keys(attributionSources).includes(sourceId) && + sourceId !== UNKNOWN_SOURCE_PLACEHOLDER + ? attributionSources[sourceId]['name'] + : sourceId; +} + +export function prepareStatistics( + attributions: Attributions, + attributionSources?: ExternalAttributionSources, +): Array { + return Object.values(attributions) + .filter((package_info) => !!package_info.licenseName) + .map((package_info) => { + return { + licenseName: package_info.licenseName as string, + criticality: package_info.criticality, + classification: package_info.classification ?? -1, + sourceName: attributionSources + ? getSourceName(package_info, attributionSources) + : undefined, + isIncomplete: isPackageInfoIncomplete(package_info), + needsReview: package_info.needsReview, + followUp: package_info.followUp, + firstParty: package_info.firstParty, + } as AttributionStatistics; + }); +} + +export function getMostFrequentLicenses2( + stats: Array, + limitDisplayed?: number, +): Array { + const licenseGroups = groupBy( + stats.filter(({ licenseName }) => !!licenseName), + ({ licenseName }) => getStrippedLicenseName(licenseName), + ); + const licensesWithCount = Object.values(licenseGroups).map((group) => { + return { + name: maxBy(group, 'licenseName')?.licenseName, + count: group.length, + } as PieChartData; + }); + if (!limitDisplayed || licensesWithCount.length <= limitDisplayed + 1) { + return licensesWithCount; + } + // sort ascending + const sortedLicensesWithCount = licensesWithCount.sort( + ({ count: c1 }, { count: c2 }) => c2 - c1, + ); + const mostFrequentLicenses = sortedLicensesWithCount.slice(0, limitDisplayed); + mostFrequentLicenses.push({ + name: 'Other', + count: sortedLicensesWithCount + .slice(limitDisplayed) + .reduce((total, { count }) => total + count, 0), + }); + return mostFrequentLicenses; +} + +export function convertToPieChartData( + data: Dictionary, +): Array { + return Object.entries(data).map(([key, value]) => { + return { + name: key, + count: value, + } as PieChartData; + }); +} + export function aggregateLicensesAndSourcesFromAttributions( attributions: Attributions, strippedLicenseNameToAttribution: UniqueLicenseNameToAttributions,