diff --git a/src/ElectronBackend/main/createWindow.ts b/src/ElectronBackend/main/createWindow.ts index 4e3f3a15b..b83d3ffac 100644 --- a/src/ElectronBackend/main/createWindow.ts +++ b/src/ElectronBackend/main/createWindow.ts @@ -9,13 +9,19 @@ import upath from 'upath'; import { getIconPath } from './iconHelpers'; +const openDevTools = (mainWindow: BrowserWindow) => { + const devtools = new BrowserWindow(); + mainWindow.webContents.setDevToolsWebContents(devtools.webContents); + mainWindow.webContents.openDevTools({ mode: 'detach' }); +}; + export async function loadWebApp( mainWindow: Electron.CrossProcessExports.BrowserWindow, ) { if (!app.isPackaged) { await mainWindow.loadURL('http://localhost:5173/'); - mainWindow.webContents.openDevTools(); + openDevTools(mainWindow); } else { await mainWindow.loadURL( `file://${path.join(upath.toUnix(__dirname), '../../index.html')}`, diff --git a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx index e5860835a..03cda4d4c 100644 --- a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx +++ b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTable.tsx @@ -7,9 +7,11 @@ import MuiTable from '@mui/material/Table'; import MuiTableBody from '@mui/material/TableBody'; import MuiTableContainer from '@mui/material/TableContainer'; import { orderBy, upperFirst } from 'lodash'; -import { useMemo, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { text } from '../../../shared/text'; +import { useShowClassifications } from '../../state/variables/use-show-classifications'; +import { useShowCriticality } from '../../state/variables/use-show-criticality'; import { LicenseCounts, LicenseNamesWithClassification, @@ -17,6 +19,7 @@ import { } from '../../types/types'; import { Order } from '../TableCellWithSorting/TableCellWithSorting'; import { + Column, ColumnConfig, orderLicenseNames, SingleColumn, @@ -32,12 +35,16 @@ const classes = { }, }; -interface AttributionCountPerSourcePerLicenseTableProps { +export interface AttributionCountPerSourcePerLicenseTableProps { licenseCounts: LicenseCounts; licenseNamesWithCriticality: LicenseNamesWithCriticality; licenseNamesWithClassification: LicenseNamesWithClassification; } +const defaultOrdering: TableOrdering = { + orderDirection: 'asc', + orderedColumn: SingleColumn.NAME, +}; export const AttributionCountPerSourcePerLicenseTable: React.FC< AttributionCountPerSourcePerLicenseTableProps > = (props) => { @@ -47,6 +54,39 @@ export const AttributionCountPerSourcePerLicenseTable: React.FC< props.licenseCounts.totalAttributionsPerSource, ); + const showCriticality = useShowCriticality(); + const showClassifications = useShowClassifications(); + + const getCriticalityColumn = useCallback((): Array => { + if (showCriticality) { + return [ + { + columnName: componentText.columns.criticality.title, + columnType: SingleColumn.CRITICALITY, + columnId: SingleColumn.CRITICALITY, + align: 'center', + defaultOrder: 'desc', + }, + ]; + } + return []; + }, [componentText.columns.criticality.title, showCriticality]); + + const getClassificationColumn = useCallback((): Array => { + if (showClassifications) { + return [ + { + columnName: componentText.columns.classification, + columnType: SingleColumn.CLASSIFICATION, + columnId: SingleColumn.CLASSIFICATION, + align: 'center', + defaultOrder: 'desc', + }, + ]; + } + return []; + }, [componentText.columns.classification, showClassifications]); + const columnConfig: ColumnConfig = useMemo( () => new ColumnConfig([ @@ -60,20 +100,8 @@ export const AttributionCountPerSourcePerLicenseTable: React.FC< align: 'left', defaultOrder: 'asc', }, - { - columnName: componentText.columns.criticality.title, - columnType: SingleColumn.CRITICALITY, - columnId: SingleColumn.CRITICALITY, - align: 'center', - defaultOrder: 'desc', - }, - { - columnName: componentText.columns.classification, - columnType: SingleColumn.CLASSIFICATION, - columnId: SingleColumn.CLASSIFICATION, - align: 'center', - defaultOrder: 'desc', - }, + ...getCriticalityColumn(), + ...getClassificationColumn(), ], }, { @@ -96,16 +124,33 @@ export const AttributionCountPerSourcePerLicenseTable: React.FC< ], }, ]), - [sourceNames, componentText], + [ + componentText.columns.licenseInfo, + componentText.columns.licenseName, + componentText.columns.signalCountPerSource, + componentText.columns.totalSources, + getCriticalityColumn, + getClassificationColumn, + sourceNames, + ], ); - const [ordering, setOrdering] = useState({ - orderDirection: 'asc', - orderedColumn: SingleColumn.NAME, - }); + const [ordering, setOrdering] = useState(defaultOrdering); + const effectiveOrdering = columnConfig.getColumnById(ordering.orderedColumn) + ? ordering + : defaultOrdering; const handleRequestSort = (columnId: string, defaultOrder: Order) => { - if (ordering.orderedColumn === columnId) { + if ( + effectiveOrdering !== ordering && + effectiveOrdering.orderedColumn === columnId + ) { + setOrdering({ + ...effectiveOrdering, + orderDirection: + effectiveOrdering.orderDirection === 'asc' ? 'desc' : 'asc', + }); + } else if (ordering.orderedColumn === columnId) { setOrdering((currentOrdering) => ({ ...currentOrdering, orderDirection: @@ -121,14 +166,14 @@ export const AttributionCountPerSourcePerLicenseTable: React.FC< const orderedLicenseNames = useMemo(() => { const orderedColumnType = columnConfig.getColumnById( - ordering.orderedColumn, + effectiveOrdering.orderedColumn, )?.columnType; if (orderedColumnType === undefined) { return orderBy( Object.keys(props.licenseNamesWithCriticality), (licenseName) => licenseName.toLowerCase(), - ordering.orderDirection, + effectiveOrdering.orderDirection, ); } @@ -136,15 +181,16 @@ export const AttributionCountPerSourcePerLicenseTable: React.FC< props.licenseNamesWithCriticality, props.licenseNamesWithClassification, props.licenseCounts, - ordering.orderDirection, + effectiveOrdering.orderDirection, orderedColumnType, ); }, [ + columnConfig, + effectiveOrdering.orderedColumn, + effectiveOrdering.orderDirection, props.licenseNamesWithCriticality, props.licenseNamesWithClassification, props.licenseCounts, - columnConfig, - ordering, ]); return ( @@ -153,7 +199,7 @@ export const AttributionCountPerSourcePerLicenseTable: React.FC< diff --git a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableHead/AttributionCountPerSourcePerLicenseTableHead.tsx b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableHead/AttributionCountPerSourcePerLicenseTableHead.tsx index cc542c22e..c5d774374 100644 --- a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableHead/AttributionCountPerSourcePerLicenseTableHead.tsx +++ b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/AttributionCountPerSourcePerLicenseTableHead/AttributionCountPerSourcePerLicenseTableHead.tsx @@ -36,7 +36,10 @@ export const AttributionCountPerSourcePerLicenseTableHead: React.FC< AttributionCountPerSourcePerLicenseTableHeadProps > = (props) => { return ( - + {props.columnConfig.groups.map((columnGroup, idx) => { return ( diff --git a/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/__tests__/AttributionCountPerSourcePerLicenseTable.test.tsx b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/__tests__/AttributionCountPerSourcePerLicenseTable.test.tsx new file mode 100644 index 000000000..43f16a184 --- /dev/null +++ b/src/Frontend/Components/AttributionCountPerSourcePerLicenseTable/__tests__/AttributionCountPerSourcePerLicenseTable.test.tsx @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 +import { Screen } from '@testing-library/dom/types/screen'; +import { act, fireEvent, screen } from '@testing-library/react'; + +import { Criticality } from '../../../../shared/shared-types'; +import { setUserSetting } from '../../../state/actions/user-settings-actions/user-settings-actions'; +import { renderComponent } from '../../../test-helpers/render'; +import { LicenseCounts } from '../../../types/types'; +import { + AttributionCountPerSourcePerLicenseTable, + AttributionCountPerSourcePerLicenseTableProps, +} from '../AttributionCountPerSourcePerLicenseTable'; + +const licenseCounts: LicenseCounts = { + attributionCountPerSourcePerLicense: { + licenseA: { + sourceA: 1, + sourceB: 3, + }, + licenseB: { + sourceB: 2, + }, + }, + totalAttributionsPerLicense: { + licenseA: 4, + licenseB: 2, + }, + totalAttributionsPerSource: { + sourceA: 1, + sourceB: 5, + }, +}; +const props: AttributionCountPerSourcePerLicenseTableProps = { + licenseCounts, + licenseNamesWithCriticality: { + licenseA: Criticality.High, + licenseB: Criticality.Medium, + }, + licenseNamesWithClassification: { + licenseA: 2, + licenseB: 3, + }, +}; + +function expectHeaderTextsToEqual( + screen: Screen, + expectedHeaderTexts: Array, +) { + const headerTexts = screen + .getAllByTestId('table-cell-with-sorting') + .map((node) => node.textContent); + + expect(headerTexts).toEqual(expectedHeaderTexts); +} + +describe('Attribution count per source per license table', () => { + it('shows by default criticality and classification columns', () => { + renderComponent(); + + expectHeaderTextsToEqual(screen, [ + 'Namesorted ascending', //correct, the sorted, ascending is for a11y + 'Criticality', + 'Classification', + 'SourceA', + 'SourceB', + 'Total', + ]); + }); + + it('does not show criticality if disabled', () => { + renderComponent(, { + actions: [setUserSetting({ showCriticality: false })], + }); + + expectHeaderTextsToEqual(screen, [ + 'Namesorted ascending', //correct, the sorted, ascending is for a11y + 'Classification', + 'SourceA', + 'SourceB', + 'Total', + ]); + }); + + it('does not show classification if disabled', () => { + renderComponent(, { + actions: [setUserSetting({ showClassifications: false })], + }); + + expectHeaderTextsToEqual(screen, [ + 'Namesorted ascending', //correct, the sorted, ascending is for a11y + 'Criticality', + 'SourceA', + 'SourceB', + 'Total', + ]); + }); + + it('switches back to default sorting if the sorted by column is dropped', () => { + const { store } = renderComponent( + , + ); + expectHeaderTextsToEqual(screen, [ + 'Namesorted ascending', //correct, the sorted, ascending is for a11y + 'Criticality', + 'Classification', + 'SourceA', + 'SourceB', + 'Total', + ]); + + fireEvent.click(screen.getByText('Criticality')); + + expectHeaderTextsToEqual(screen, [ + 'Name', + 'Criticalitysorted descending', //correct, the sorted, descending is for a11y + 'Classification', + 'SourceA', + 'SourceB', + 'Total', + ]); + + act(() => { + store.dispatch(setUserSetting({ showCriticality: false })); + }); + + expectHeaderTextsToEqual(screen, [ + 'Namesorted ascending', //correct, the sorted, ascending is for a11y + 'Classification', + 'SourceA', + 'SourceB', + 'Total', + ]); + + fireEvent.click(screen.getByText('Name')); + + expectHeaderTextsToEqual(screen, [ + 'Namesorted descending', //correct, the sorted, descending is for a11y + 'Classification', + 'SourceA', + 'SourceB', + 'Total', + ]); + }); +}); diff --git a/src/Frontend/Components/TableCellWithSorting/TableCellWithSorting.tsx b/src/Frontend/Components/TableCellWithSorting/TableCellWithSorting.tsx index 70b454ced..84ad67510 100644 --- a/src/Frontend/Components/TableCellWithSorting/TableCellWithSorting.tsx +++ b/src/Frontend/Components/TableCellWithSorting/TableCellWithSorting.tsx @@ -32,6 +32,7 @@ export const TableCellWithSorting: React.FC = ( ...props.sx, }} sortDirection={props.isSortedColumn ? props.order : false} + data-testid="table-cell-with-sorting" >