From 6cf7088066fa0763ab7900cf19dc27bac65e3584 Mon Sep 17 00:00:00 2001 From: daniele-mng Date: Tue, 19 May 2026 15:30:57 +0200 Subject: [PATCH 1/6] refactor: operating system tab and tables to typescript - add test for the components --- .../reports/details/OperatingSystemsTab.jsx | 79 ---- .../reports/details/OperatingSystemsTab.tsx | 100 +++++ .../reports/details/OperatingSystemsTable.jsx | 124 ------ .../reports/details/OperatingSystemsTable.tsx | 133 ++++++ .../__tests__/OperatingSystemsTable.test.tsx | 410 ++++++++++++++++++ 5 files changed, 643 insertions(+), 203 deletions(-) delete mode 100644 src/web/pages/reports/details/OperatingSystemsTab.jsx create mode 100644 src/web/pages/reports/details/OperatingSystemsTab.tsx delete mode 100644 src/web/pages/reports/details/OperatingSystemsTable.jsx create mode 100644 src/web/pages/reports/details/OperatingSystemsTable.tsx create mode 100644 src/web/pages/reports/details/__tests__/OperatingSystemsTable.test.tsx diff --git a/src/web/pages/reports/details/OperatingSystemsTab.jsx b/src/web/pages/reports/details/OperatingSystemsTab.jsx deleted file mode 100644 index efcece203c..0000000000 --- a/src/web/pages/reports/details/OperatingSystemsTab.jsx +++ /dev/null @@ -1,79 +0,0 @@ -/* SPDX-FileCopyrightText: 2024 Greenbone AG - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import React from 'react'; -import OperatingSystemsTable from 'web/pages/reports/details/OperatingSystemsTable'; -import ReportEntitiesContainer from 'web/pages/reports/details/ReportEntitiesContainer'; -import PropTypes from 'web/utils/PropTypes'; -import {makeCompareNumber, makeCompareString} from 'web/utils/Sort'; - -const operatingssystemsSortFunctions = { - name: makeCompareString('name'), - cpe: makeCompareString('id'), - hosts: makeCompareNumber(entity => entity.hosts.count), - severity: makeCompareNumber('severity', 0), - compliant: makeCompareString('compliance'), -}; - -const OperatingSystemsTab = ({ - audit = false, - counts, - filter, - operatingsystems, - isUpdating, - sortField, - sortReverse, - - onSortChange, -}) => ( - - {({ - entities, - entitiesCounts, - sortBy, - sortDir, - onFirstClick, - onLastClick, - onNextClick, - onPreviousClick, - }) => ( - - )} - -); - -OperatingSystemsTab.propTypes = { - audit: PropTypes.bool, - counts: PropTypes.object, - filter: PropTypes.filter.isRequired, - isUpdating: PropTypes.bool, - operatingsystems: PropTypes.array, - sortField: PropTypes.string.isRequired, - sortReverse: PropTypes.bool.isRequired, - onSortChange: PropTypes.func.isRequired, -}; - -export default OperatingSystemsTab; diff --git a/src/web/pages/reports/details/OperatingSystemsTab.tsx b/src/web/pages/reports/details/OperatingSystemsTab.tsx new file mode 100644 index 0000000000..d4019d63c6 --- /dev/null +++ b/src/web/pages/reports/details/OperatingSystemsTab.tsx @@ -0,0 +1,100 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type CollectionCounts from 'gmp/collection/collection-counts'; +import type Filter from 'gmp/models/filter'; +import type ReportOperatingSystem from 'gmp/models/report/os'; +import OperatingSystemsTable from 'web/pages/reports/details/OperatingSystemsTable'; +import ReportEntitiesContainer from 'web/pages/reports/details/ReportEntitiesContainer'; +import {makeCompareNumber, makeCompareString} from 'web/utils/Sort'; + +type OperatingSystemsSortFunctions = { + name: ( + sortReverse?: boolean, + ) => (a: ReportOperatingSystem, b: ReportOperatingSystem) => number; + cpe: ( + sortReverse?: boolean, + ) => (a: ReportOperatingSystem, b: ReportOperatingSystem) => number; + hosts: ( + sortReverse?: boolean, + ) => (a: ReportOperatingSystem, b: ReportOperatingSystem) => number; + severity: ( + sortReverse?: boolean, + ) => (a: ReportOperatingSystem, b: ReportOperatingSystem) => number; + compliant: ( + sortReverse?: boolean, + ) => (a: ReportOperatingSystem, b: ReportOperatingSystem) => number; +}; + +interface OperatingSystemsTabProps { + audit?: boolean; + counts?: CollectionCounts; + filter: Filter; + isUpdating?: boolean; + operatingsystems?: ReportOperatingSystem[]; + sortField: string; + sortReverse: boolean; + onSortChange: (sortField: string) => void; +} + +const operatingssystemsSortFunctions: OperatingSystemsSortFunctions = { + name: makeCompareString('name'), + cpe: makeCompareString('id'), + hosts: makeCompareNumber(entity => entity.hosts.count), + severity: makeCompareNumber('severity', 0), + compliant: makeCompareString('compliance'), +}; + +const OperatingSystemsTab = ({ + audit = false, + counts, + filter, + operatingsystems, + isUpdating, + sortField, + sortReverse, + onSortChange, +}: OperatingSystemsTabProps) => { + return ( + + counts={counts} + entities={operatingsystems} + filter={filter} + sortField={sortField} + sortFunctions={operatingssystemsSortFunctions} + sortReverse={sortReverse} + > + {({ + entities, + entitiesCounts, + sortBy, + sortDir, + onFirstClick, + onLastClick, + onNextClick, + onPreviousClick, + }) => ( + + )} + + ); +}; + +export default OperatingSystemsTab; diff --git a/src/web/pages/reports/details/OperatingSystemsTable.jsx b/src/web/pages/reports/details/OperatingSystemsTable.jsx deleted file mode 100644 index c8c68f3f87..0000000000 --- a/src/web/pages/reports/details/OperatingSystemsTable.jsx +++ /dev/null @@ -1,124 +0,0 @@ -/* SPDX-FileCopyrightText: 2024 Greenbone AG - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import React from 'react'; -import {_, _l} from 'gmp/locale/lang'; -import {isDefined} from 'gmp/utils/identity'; -import ComplianceBar from 'web/components/bar/ComplianceBar'; -import SeverityBar from 'web/components/bar/SeverityBar'; -import OsIcon from 'web/components/icon/OsIcon'; -import IconDivider from 'web/components/layout/IconDivider'; -import Link from 'web/components/link/Link'; -import TableData from 'web/components/table/TableData'; -import TableHead from 'web/components/table/TableHead'; -import TableHeader from 'web/components/table/TableHeader'; -import TableRow from 'web/components/table/TableRow'; -import createEntitiesTable from 'web/entities/createEntitiesTable'; -import PropTypes from 'web/utils/PropTypes'; - -const Header = ({ - audit = false, - currentSortDir, - currentSortBy, - sort = true, - onSortChange, -}) => ( - - - - - - {audit ? ( - - ) : ( - - )} - - -); - -Header.propTypes = { - audit: PropTypes.bool, - currentSortBy: PropTypes.string, - currentSortDir: PropTypes.string, - sort: PropTypes.bool, - onSortChange: PropTypes.func, -}; - -const Row = ({audit = false, entity, links = true}) => { - const {name, cpe, hosts, severity, compliance} = entity; - return ( - - - - - - {name} - - - - - - {cpe} - - - {hosts.count} - {audit && isDefined(compliance) ? ( - - - - ) : ( - - - - )} - - ); -}; - -Row.propTypes = { - audit: PropTypes.bool, - entity: PropTypes.object.isRequired, - links: PropTypes.bool, -}; - -export default createEntitiesTable({ - header: Header, - emptyTitle: _l('No Operating Systems available'), - row: Row, -}); diff --git a/src/web/pages/reports/details/OperatingSystemsTable.tsx b/src/web/pages/reports/details/OperatingSystemsTable.tsx new file mode 100644 index 0000000000..9be0c88edf --- /dev/null +++ b/src/web/pages/reports/details/OperatingSystemsTable.tsx @@ -0,0 +1,133 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {_, _l} from 'gmp/locale/lang'; +import {COMPLIANCE} from 'gmp/models/compliance'; +import type ReportOperatingSystem from 'gmp/models/report/os'; +import ComplianceBar from 'web/components/bar/ComplianceBar'; +import SeverityBar from 'web/components/bar/SeverityBar'; +import OsIcon from 'web/components/icon/OsIcon'; +import TableData from 'web/components/table/TableData'; +import TableHead from 'web/components/table/TableHead'; +import TableHeader from 'web/components/table/TableHeader'; +import TableRow from 'web/components/table/TableRow'; +import createEntitiesTable from 'web/entities/createEntitiesTable'; +import {type SortDirectionType} from 'web/utils/sort-direction'; + +interface HeaderProps { + audit?: boolean; + currentSortBy?: string; + currentSortDir?: SortDirectionType; + sort?: boolean; + onSortChange?: (sortBy: string) => void; +} + +const getColumns = (audit = false) => [ + { + key: 'name', + title: _('Operating System'), + sortBy: 'name', + render: (entity: ReportOperatingSystem) => ( + + ), + align: 'center', + }, + { + key: 'cpe', + title: _('CPE'), + sortBy: 'cpe', + render: (entity: ReportOperatingSystem) => entity.cpe, + align: 'center', + }, + { + key: 'hosts', + title: _('Hosts'), + width: '10%', + sortBy: 'hosts', + render: (entity: ReportOperatingSystem) => entity.hosts?.count ?? 0, + align: 'center', + }, + ...(audit + ? [ + { + key: 'compliance', + title: _('Compliant'), + width: '10%', + sortBy: 'compliance', + render: (entity: ReportOperatingSystem) => ( + + ), + align: 'center', + }, + ] + : [ + { + key: 'severity', + title: _('Severity'), + width: '10%', + sortBy: 'severity', + render: (entity: ReportOperatingSystem) => ( + + ), + align: 'center', + }, + ]), +]; + +const Header = ({ + audit = false, + currentSortBy, + currentSortDir, + sort = true, + onSortChange, +}: HeaderProps) => { + const columns = getColumns(audit); + + return ( + + + {columns.map(column => ( + + {column.render({ + compliance: COMPLIANCE.UNDEFINED, + hosts: {count: 0, hostsByIp: {}, complianceByIp: {}}, + severity: undefined, + cpe: '', + name: '', + } as ReportOperatingSystem)} + + ))} + + + ); +}; + +const Row = ({entity, audit = false}) => { + const columns = getColumns(audit); + + return ( + + {columns.map(column => ( + + {column.render(entity)} + + ))} + + ); +}; + +export default createEntitiesTable({ + header: Header, + emptyTitle: _l('No Operating Systems available'), + row: Row, +}); diff --git a/src/web/pages/reports/details/__tests__/OperatingSystemsTable.test.tsx b/src/web/pages/reports/details/__tests__/OperatingSystemsTable.test.tsx new file mode 100644 index 0000000000..dca1d10750 --- /dev/null +++ b/src/web/pages/reports/details/__tests__/OperatingSystemsTable.test.tsx @@ -0,0 +1,410 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, expect, test, testing} from '@gsa/testing'; +import {rendererWith, screen, within, userEvent} from 'web/testing'; +import CollectionCounts from 'gmp/collection/collection-counts'; +import Filter from 'gmp/models/filter'; +import {SEVERITY_RATING_CVSS_3} from 'gmp/utils/severity'; +import {getMockAuditReport} from 'web/pages/reports/__fixtures__/MockAuditReport'; +import {getMockReport} from 'web/pages/reports/__fixtures__/MockReport'; +import OperatingSystemsTable from 'web/pages/reports/details/OperatingSystemsTable'; + +const filter = Filter.fromString('first=1 rows=10'); + +const createGmp = () => ({ + session: {timezone: 'CET'}, + + settings: { + severityRating: SEVERITY_RATING_CVSS_3, + }, +}); + +describe('OperatingSystemsTable', () => { + test('should render table with all columns', () => { + const {operatingsystems} = getMockReport(); + + const {render} = rendererWith({ + router: true, + + gmp: createGmp(), + }); + + render( + , + ); + + const table = screen.getByRole('table'); + + const columnHeaders = within(table).getAllByRole('columnheader'); + + expect( + columnHeaders.some(th => /Operating System/i.exec(th.textContent)), + ).toBe(true); + + expect(columnHeaders.some(th => /CPE/i.exec(th.textContent))).toBe(true); + + expect(columnHeaders.some(th => /Hosts/i.exec(th.textContent))).toBe(true); + + expect(columnHeaders.some(th => /Severity/i.exec(th.textContent))).toBe( + true, + ); + }); + + test('should render operating system data correctly', () => { + const {operatingsystems} = getMockReport(); + + const {render} = rendererWith({ + router: true, + + gmp: createGmp(), + }); + + render( + , + ); + + // Should render OS name and CPE + + screen.getByText('Foo OS'); + + screen.getByText('cpe:/foo/bar'); + + // Should render severity bar + + screen.getByText('10.0 (Critical)'); + }); + + test('should render operating system as link when CPE is defined', () => { + const {operatingsystems} = getMockReport(); + + const {render} = rendererWith({ + router: true, + + gmp: createGmp(), + }); + + render( + , + ); + + const osLink = screen.getByText('Foo OS'); + + expect(osLink.closest('a')).toHaveAttribute( + 'href', + + '/operatingsystems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', + ); + }); + + test('should render CPE as link', () => { + const {operatingsystems} = getMockReport(); + + const {render} = rendererWith({ + router: true, + + gmp: createGmp(), + }); + + render( + , + ); + + const cpeLink = screen.getByText('cpe:/foo/bar'); + + expect(cpeLink.closest('a')).toHaveAttribute( + 'href', + + '/operatingsystems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', + ); + }); + + test('should render severity bar instead of compliance bar for regular reports', () => { + const {operatingsystems} = getMockReport(); + + const {render} = rendererWith({ + router: true, + + gmp: createGmp(), + }); + + render( + , + ); + + // Should render severity bars, not compliance bars + + screen.getByText('10.0 (Critical)'); + + screen.getByText('5.0 (Medium)'); + }); + + test('should render empty state when no operating systems', () => { + const counts = new CollectionCounts({ + filtered: 0, + + all: 0, + + first: 1, + + rows: 10, + }); + + const {render} = rendererWith({ + router: true, + + gmp: createGmp(), + }); + + render( + , + ); + + screen.getByText('No Operating Systems available'); + }); + + test('should render audit report with compliance column', () => { + const {operatingsystems} = getMockAuditReport(); + + const auditFilter = Filter.fromString('first=1 rows=10'); + + const {render} = rendererWith({ + router: true, + + gmp: createGmp(), + }); + + render( + , + ); + + const table = screen.getByRole('table'); + + const columnHeaders = within(table).getAllByRole('columnheader'); + + expect(columnHeaders).toHaveLength(4); + + expect( + columnHeaders.some(th => /Operating System/i.exec(th.textContent)), + ).toBe(true); + + expect(columnHeaders.some(th => /CPE/i.exec(th.textContent))).toBe(true); + + expect(columnHeaders.some(th => /Hosts/i.exec(th.textContent))).toBe(true); + + expect(columnHeaders.some(th => /Compliant/i.exec(th.textContent))).toBe( + true, + ); + }); + + test('should render compliance bar instead of severity bar for audit reports', () => { + const {operatingsystems} = getMockAuditReport(); + + const auditFilter = Filter.fromString('first=1 rows=10'); + + const {render} = rendererWith({ + router: true, + + gmp: createGmp(), + }); + + render( + , + ); + + // Should render compliance bars, not severity bars + + screen.getByText('No'); + + screen.getByText('Incomplete'); + }); + + test('should handle sorting by operating system column', async () => { + const {operatingsystems} = getMockReport(); + + const onSortChange = testing.fn(); + + const {render} = rendererWith({ + router: true, + + gmp: createGmp(), + }); + + render( + , + ); + + const osHeader = await screen.findByText('Operating System'); + + await userEvent.click(osHeader); + + expect(onSortChange).toHaveBeenCalledWith('name'); + }); + + test('should handle sorting by CPE column', async () => { + const {operatingsystems} = getMockReport(); + + const onSortChange = testing.fn(); + + const {render} = rendererWith({ + router: true, + + gmp: createGmp(), + }); + + render( + , + ); + + const cpeHeader = await screen.findByText('CPE'); + + await userEvent.click(cpeHeader); + + expect(onSortChange).toHaveBeenCalledWith('cpe'); + }); + + test('should handle sorting by hosts column', async () => { + const {operatingsystems} = getMockReport(); + + const onSortChange = testing.fn(); + + const {render} = rendererWith({ + router: true, + + gmp: createGmp(), + }); + + render( + , + ); + + const hostsHeader = await screen.findByText('Hosts'); + + await userEvent.click(hostsHeader); + + expect(onSortChange).toHaveBeenCalledWith('hosts'); + }); + + test('should handle sorting by severity column', async () => { + const {operatingsystems} = getMockReport(); + + const onSortChange = testing.fn(); + + const {render} = rendererWith({ + router: true, + + gmp: createGmp(), + }); + + render( + , + ); + + const severityHeader = await screen.findByText('Severity'); + + await userEvent.click(severityHeader); + + expect(onSortChange).toHaveBeenCalledWith('severity'); + }); + + test('should not render severity column when audit is true', () => { + const {operatingsystems} = getMockAuditReport(); + + const auditFilter = Filter.fromString('first=1 rows=10'); + + const {render} = rendererWith({ + router: true, + + gmp: createGmp(), + }); + + render( + , + ); + + const table = screen.getByRole('table'); + + const columnHeaders = within(table).getAllByRole('columnheader'); + + expect(columnHeaders.some(th => /Severity/i.exec(th.textContent))).toBe( + false, + ); + + expect(columnHeaders.some(th => /Compliant/i.exec(th.textContent))).toBe( + true, + ); + }); +}); From 9e0cb393885ed9c0631f8cbcf6fb009a202fde31 Mon Sep 17 00:00:00 2001 From: daniele-mng Date: Tue, 19 May 2026 18:36:28 +0200 Subject: [PATCH 2/6] add: report operating system gmp command --- .../__tests__/report-operating-system.test.ts | 109 ++++++++++++++ src/gmp/commands/report-operating-system.ts | 93 ++++++++++++ src/gmp/gmp.ts | 3 + .../report-operating-system.test.tsx | 117 +++++++++++++++ .../use-query/report-operating-system.ts | 35 +++++ .../reports/AuditReportDetailsContent.tsx | 8 +- src/web/pages/reports/DetailsContent.tsx | 8 +- .../reports/details/OperatingSystemsTab.tsx | 133 ++++++++++++----- .../reports/details/OperatingSystemsTable.tsx | 38 +++-- .../__tests__/OperatingSystemsTab.test.jsx | 139 +++++++++++------- 10 files changed, 567 insertions(+), 116 deletions(-) create mode 100644 src/gmp/commands/__tests__/report-operating-system.test.ts create mode 100644 src/gmp/commands/report-operating-system.ts create mode 100644 src/web/hooks/use-query/__tests__/report-operating-system.test.tsx create mode 100644 src/web/hooks/use-query/report-operating-system.ts diff --git a/src/gmp/commands/__tests__/report-operating-system.test.ts b/src/gmp/commands/__tests__/report-operating-system.test.ts new file mode 100644 index 0000000000..4d4d2121dd --- /dev/null +++ b/src/gmp/commands/__tests__/report-operating-system.test.ts @@ -0,0 +1,109 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, test, expect} from '@gsa/testing'; +import ReportOperatingSystemsCommand from 'gmp/commands/report-operating-system'; +import {createHttp, createResponse} from 'gmp/commands/testing'; + +const makeResponse = (operatingSystems: object) => + createResponse({ + get_report_operating_systems: { + get_report_operating_systems_response: { + operating_systems: operatingSystems, + }, + }, + }); + +describe('ReportOperatingSystemsCommand tests', () => { + test('should fetch and parse multiple OS entities', async () => { + const response = makeResponse({ + operating_system: [ + {best_os_cpe: 'cpe:/foo/bar', best_os_txt: 'Foo OS', hosts_count: '2'}, + { + best_os_cpe: 'cpe:/lorem/ipsum', + best_os_txt: 'Lorem OS', + hosts_count: '5', + }, + ], + }); + const fakeHttp = createHttp(response); + const cmd = new ReportOperatingSystemsCommand(fakeHttp); + + const result = await cmd.get({report_id: '1234'}); + + expect(fakeHttp.request).toHaveBeenCalledWith('get', { + args: { + cmd: 'get_report_operating_systems', + details: 1, + report_id: '1234', + }, + }); + + expect(result.data).toHaveLength(2); + expect(result.data[0].cpe).toBe('cpe:/foo/bar'); + expect(result.data[0].name).toBe('Foo OS'); + expect(result.data[0].hosts.count).toBe(2); + expect(result.data[1].cpe).toBe('cpe:/lorem/ipsum'); + expect(result.data[1].name).toBe('Lorem OS'); + expect(result.data[1].hosts.count).toBe(5); + }); + + test('should fetch and parse a single OS entity (non-array from XML)', async () => { + const response = makeResponse({ + operating_system: { + best_os_cpe: 'cpe:/single/os', + best_os_txt: 'Single OS', + hosts_count: '3', + }, + }); + const fakeHttp = createHttp(response); + const cmd = new ReportOperatingSystemsCommand(fakeHttp); + + const result = await cmd.get({report_id: '1234'}); + + expect(result.data).toHaveLength(1); + expect(result.data[0].cpe).toBe('cpe:/single/os'); + expect(result.data[0].name).toBe('Single OS'); + expect(result.data[0].hosts.count).toBe(3); + }); + + test('should return correct CollectionCounts', async () => { + const response = makeResponse({ + operating_system: [ + {best_os_cpe: 'cpe:/a', best_os_txt: 'OS A', hosts_count: '1'}, + {best_os_cpe: 'cpe:/b', best_os_txt: 'OS B', hosts_count: '10'}, + ], + }); + const fakeHttp = createHttp(response); + const cmd = new ReportOperatingSystemsCommand(fakeHttp); + + const result = await cmd.get({report_id: '1234'}); + + expect(result.meta.counts.all).toBe(2); + expect(result.meta.counts.filtered).toBe(2); + expect(result.meta.counts.length).toBe(2); + }); + + test('should handle empty operating_systems', async () => { + const response = makeResponse({}); + const fakeHttp = createHttp(response); + const cmd = new ReportOperatingSystemsCommand(fakeHttp); + + const result = await cmd.get({report_id: '1234'}); + + expect(result.data).toHaveLength(0); + expect(result.meta.counts.all).toBe(0); + }); + + test('should throw when response wrapper is missing', async () => { + const response = createResponse({}); + const fakeHttp = createHttp(response); + const cmd = new ReportOperatingSystemsCommand(fakeHttp); + + await expect(cmd.get({report_id: '1234'})).rejects.toThrow( + 'Invalid response: get_report_operating_systems not found in response', + ); + }); +}); diff --git a/src/gmp/commands/report-operating-system.ts b/src/gmp/commands/report-operating-system.ts new file mode 100644 index 0000000000..29c2661bca --- /dev/null +++ b/src/gmp/commands/report-operating-system.ts @@ -0,0 +1,93 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import CollectionCounts from 'gmp/collection/collection-counts'; +import {parseFilter} from 'gmp/collection/parser'; +import type {EntitiesMeta} from 'gmp/commands/entities'; +import HttpCommand, { + type HttpCommandInputParams, + type HttpCommandOptions, +} from 'gmp/commands/http'; +import type Http from 'gmp/http/http'; +import type Response from 'gmp/http/response'; +import type {XmlResponseData} from 'gmp/http/transform/fast-xml'; +import ReportOperatingSystem from 'gmp/models/report/os'; +import {map} from 'gmp/utils/array'; + +interface OperatingSystemElement { + best_os_cpe?: string; + best_os_txt?: string; + hosts_count?: string | number; +} + +interface ReportOperatingSystemsResponseData extends XmlResponseData { + get_report_operating_systems?: { + get_report_operating_systems_response?: { + operating_systems?: { + operating_system?: + | OperatingSystemElement + | OperatingSystemElement[]; + }; + }; + }; +} + +class ReportOperatingSystemsCommand extends HttpCommand { + constructor(http: Http) { + super(http, {cmd: 'get_report_operating_systems'}); + } + + async get( + params: HttpCommandInputParams = {}, + options?: HttpCommandOptions, + ): Promise> { + const response = await this.httpGetWithTransform( + {details: 1, ...params}, + options, + ); + + const root = response.data as ReportOperatingSystemsResponseData; + + if (!root.get_report_operating_systems) { + throw new Error( + 'Invalid response: get_report_operating_systems not found in response', + ); + } + + const data = + root.get_report_operating_systems.get_report_operating_systems_response; + + const filter = parseFilter(data as any); + + const entities = map( + data?.operating_systems?.operating_system, + (item: OperatingSystemElement) => { + const os = ReportOperatingSystem.fromElement({ + best_os_cpe: item.best_os_cpe, + best_os_txt: item.best_os_txt, + }); + os.hosts.count = Number(item.hosts_count) || 0; + return os; + }, + ); + + const filteredCount = entities.length; + const counts = new CollectionCounts({ + all: filteredCount, + filtered: filteredCount, + first: 1, + length: filteredCount, + rows: filteredCount, + }); + + return response.set(entities, { + filter, + counts, + }); + } +} + +export default ReportOperatingSystemsCommand; + diff --git a/src/gmp/gmp.ts b/src/gmp/gmp.ts index 53782d6ac1..b7db9d0fa5 100644 --- a/src/gmp/gmp.ts +++ b/src/gmp/gmp.ts @@ -61,6 +61,7 @@ import ReportConfigsCommand from 'gmp/commands/report-configs'; import ReportsErrorsCommand from 'gmp/commands/report-errors'; import ReportFormatCommand from 'gmp/commands/report-format'; import ReportFormatsCommand from 'gmp/commands/report-formats'; +import ReportOperatingSystemsCommand from 'gmp/commands/report-operating-system'; import ReportPortsCommand from 'gmp/commands/report-ports'; import ReportTlsCertificatesCommand from 'gmp/commands/report-tls-certificates'; import ReportsCommand from 'gmp/commands/reports'; @@ -150,6 +151,7 @@ class Gmp { public readonly reportformats: ReportFormatsCommand; public readonly reports: ReportsCommand; public readonly reportports: ReportPortsCommand; + public readonly reportoperatingsystems: ReportOperatingSystemsCommand; public readonly reporttlscertificates: ReportTlsCertificatesCommand; public readonly result: ResultCommand; public readonly results: ResultsCommand; @@ -239,6 +241,7 @@ class Gmp { this.reporterrors = new ReportsErrorsCommand(this.http); this.reportformat = new ReportFormatCommand(this.http); this.reportformats = new ReportFormatsCommand(this.http); + this.reportoperatingsystems = new ReportOperatingSystemsCommand(this.http); this.reports = new ReportsCommand(this.http); this.reportports = new ReportPortsCommand(this.http); this.reporttlscertificates = new ReportTlsCertificatesCommand(this.http); diff --git a/src/web/hooks/use-query/__tests__/report-operating-system.test.tsx b/src/web/hooks/use-query/__tests__/report-operating-system.test.tsx new file mode 100644 index 0000000000..131a6f8fd1 --- /dev/null +++ b/src/web/hooks/use-query/__tests__/report-operating-system.test.tsx @@ -0,0 +1,117 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, test, expect, testing} from '@gsa/testing'; +import {rendererWith, screen, waitFor} from 'web/testing'; +import CollectionCounts from 'gmp/collection/collection-counts'; +import Filter from 'gmp/models/filter'; +import ReportOperatingSystem from 'gmp/models/report/os'; +import {createSession} from 'gmp/testing'; +import {SEVERITY_RATING_CVSS_3} from 'gmp/utils/severity'; +import {useGetReportOperatingSystems} from 'web/hooks/use-query/report-operating-system'; + +const os1 = ReportOperatingSystem.fromElement({ + best_os_cpe: 'cpe:/foo/bar', + best_os_txt: 'Foo OS', +}); +os1.hosts.count = 2; + +const os2 = ReportOperatingSystem.fromElement({ + best_os_cpe: 'cpe:/lorem/ipsum', + best_os_txt: 'Lorem OS', +}); +os2.hosts.count = 5; + +const filter = Filter.fromString('rows=10 first=1'); + +const TestComponent = ({ + reportId, + filter: testFilter, +}: { + reportId: string; + filter?: Filter; +}) => { + const {data, isLoading, isError} = useGetReportOperatingSystems({ + reportId, + filter: testFilter, + }); + + if (isLoading) { + return
Loading...
; + } + if (isError) { + return
Error
; + } + if (!data) { + return
No data
; + } + + return ( +
+ {data.entities.map(os => ( +
+ {os.name} +
+ ))} +
+ ); +}; + +const createGmp = () => ({ + session: createSession({token: 'test-token'}), + settings: {severityRating: SEVERITY_RATING_CVSS_3}, + reportoperatingsystems: { + get: testing.fn().mockResolvedValue({ + data: [os1, os2], + meta: { + filter, + counts: new CollectionCounts({all: 2, filtered: 2, length: 2}), + }, + }), + }, +}); + +describe('useGetReportOperatingSystems', () => { + test('should fetch OS entities for a given reportId', async () => { + const gmp = createGmp(); + const {render} = rendererWith({gmp, router: true}); + + render(); + + await waitFor(() => { + expect(screen.getAllByTestId('os-entity')).toHaveLength(2); + }); + + expect(gmp.reportoperatingsystems.get).toHaveBeenCalledWith( + expect.objectContaining({report_id: '1234'}), + ); + expect(screen.getByText('Foo OS')).toBeInTheDocument(); + expect(screen.getByText('Lorem OS')).toBeInTheDocument(); + }); + + test('should show loading state initially', () => { + const gmp = createGmp(); + // Replace with a promise that never resolves to keep loading state + gmp.reportoperatingsystems.get = testing + .fn() + .mockReturnValue(new Promise(() => {})); + + const {render} = rendererWith({gmp, router: true}); + render(); + + expect(screen.getByTestId('loading')).toBeInTheDocument(); + }); + + test('should not fetch when reportId is empty', () => { + const gmp = createGmp(); + const {render} = rendererWith({gmp, router: true}); + + render(); + + // Query is disabled when reportId is empty — no fetch is triggered + expect(screen.getByTestId('no-data')).toBeInTheDocument(); + expect(gmp.reportoperatingsystems.get).not.toHaveBeenCalled(); + }); +}); diff --git a/src/web/hooks/use-query/report-operating-system.ts b/src/web/hooks/use-query/report-operating-system.ts new file mode 100644 index 0000000000..82394ebfd6 --- /dev/null +++ b/src/web/hooks/use-query/report-operating-system.ts @@ -0,0 +1,35 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type Filter from 'gmp/models/filter'; +import type ReportOperatingSystem from 'gmp/models/report/os'; +import useGmp from 'web/hooks/useGmp'; +import useGetEntities from 'web/queries/useGetEntities'; + +interface UseGetReportOperatingSystemsParams { + reportId: string; + filter?: Filter; +} + +export const useGetReportOperatingSystems = ({ + reportId, + filter, +}: UseGetReportOperatingSystemsParams) => { + const gmp = useGmp(); + + return useGetEntities({ + gmpMethod: ({filter: reportFilter}) => + gmp.reportoperatingsystems.get({ + report_id: reportId, + filter: reportFilter, + }), + queryId: `get_report_operating_systems_${reportId}`, + filter, + enabled: Boolean(reportId), + keepPreviousData: true, + }); +}; + +export default useGetReportOperatingSystems; diff --git a/src/web/pages/reports/AuditReportDetailsContent.tsx b/src/web/pages/reports/AuditReportDetailsContent.tsx index dc4a3d8f68..2f088be031 100644 --- a/src/web/pages/reports/AuditReportDetailsContent.tsx +++ b/src/web/pages/reports/AuditReportDetailsContent.tsx @@ -302,13 +302,9 @@ const AuditReportDetailsContent = ({ thresholdConfig, onSortChange('os', sortField)} + reportId={reportId} + reportOperatingSystems={operatingSystems?.entities} />, ), }, diff --git a/src/web/pages/reports/DetailsContent.tsx b/src/web/pages/reports/DetailsContent.tsx index 98ab9330ca..f8c1118e49 100644 --- a/src/web/pages/reports/DetailsContent.tsx +++ b/src/web/pages/reports/DetailsContent.tsx @@ -385,13 +385,9 @@ const PageContent = ({ activeFilter, thresholdConfig, onSortChange('os', sortField)} + reportId={reportId} + reportOperatingSystems={operatingsystems?.entities} />, ), }, diff --git a/src/web/pages/reports/details/OperatingSystemsTab.tsx b/src/web/pages/reports/details/OperatingSystemsTab.tsx index d4019d63c6..ec788f63a6 100644 --- a/src/web/pages/reports/details/OperatingSystemsTab.tsx +++ b/src/web/pages/reports/details/OperatingSystemsTab.tsx @@ -3,13 +3,26 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type CollectionCounts from 'gmp/collection/collection-counts'; -import type Filter from 'gmp/models/filter'; -import type ReportOperatingSystem from 'gmp/models/report/os'; +import {useMemo, useState} from 'react'; +import {useTranslation} from 'react-i18next'; +import Filter from 'gmp/models/filter'; +import ReportOperatingSystem from 'gmp/models/report/os'; +import {isDefined} from 'gmp/utils/identity'; +import Loading from 'web/components/loading/Loading'; +import useGetReportOperatingSystems from 'web/hooks/use-query/report-operating-system'; +import useFilterSortBy from 'web/hooks/useFilterSortBy'; import OperatingSystemsTable from 'web/pages/reports/details/OperatingSystemsTable'; import ReportEntitiesContainer from 'web/pages/reports/details/ReportEntitiesContainer'; import {makeCompareNumber, makeCompareString} from 'web/utils/Sort'; +interface OperatingSystemsTabWrapperProps { + audit?: boolean; + filter?: Filter; + reportId: string; + /** Pre-parsed OS entities from the full report, used to enrich severity and compliance. */ + reportOperatingSystems?: ReportOperatingSystem[]; +} + type OperatingSystemsSortFunctions = { name: ( sortReverse?: boolean, @@ -28,18 +41,7 @@ type OperatingSystemsSortFunctions = { ) => (a: ReportOperatingSystem, b: ReportOperatingSystem) => number; }; -interface OperatingSystemsTabProps { - audit?: boolean; - counts?: CollectionCounts; - filter: Filter; - isUpdating?: boolean; - operatingsystems?: ReportOperatingSystem[]; - sortField: string; - sortReverse: boolean; - onSortChange: (sortField: string) => void; -} - -const operatingssystemsSortFunctions: OperatingSystemsSortFunctions = { +const operatingSystemsSortFunctions: OperatingSystemsSortFunctions = { name: makeCompareString('name'), cpe: makeCompareString('id'), hosts: makeCompareNumber(entity => entity.hosts.count), @@ -47,24 +49,85 @@ const operatingssystemsSortFunctions: OperatingSystemsSortFunctions = { compliant: makeCompareString('compliance'), }; -const OperatingSystemsTab = ({ - audit = false, - counts, +const OperatingSystemsTabWrapper = ({ filter, - operatingsystems, - isUpdating, - sortField, - sortReverse, - onSortChange, -}: OperatingSystemsTabProps) => { + reportId, + audit = false, + reportOperatingSystems, +}: OperatingSystemsTabWrapperProps) => { + const [_] = useTranslation(); + + const baseFilter = useMemo(() => { + return isDefined(filter) ? filter.copy() : new Filter(); + }, [filter]); + + const [operatingSystemsFilter, setOperatingSystemsFilter] = + useState(baseFilter); + + const {data, isLoading, isFetching, isError} = useGetReportOperatingSystems({ + reportId, + filter: operatingSystemsFilter, + }); + + const updateFilter = (newFilter: Filter) => { + setOperatingSystemsFilter(newFilter); + }; + + const [sortBy, sortDir, handleSortChange] = useFilterSortBy( + operatingSystemsFilter, + updateFilter, + ); + + // Enrich entities from the dedicated endpoint with severity / compliance + // data from the full report parse, which has that information. + const operatingSystems = useMemo(() => { + const fetchedEntities = data?.entities ?? []; + if (!reportOperatingSystems?.length || !fetchedEntities.length) { + return fetchedEntities; + } + const byKey = new Map(reportOperatingSystems.map(os => [os.cpe, os])); + return fetchedEntities.map(os => { + const source = byKey.get(os.cpe); + if (!isDefined(source)) return os; + const enriched = ReportOperatingSystem.fromElement({ + best_os_cpe: os.cpe, + best_os_txt: os.name, + }); + enriched.hosts.count = os.hosts.count; + if (isDefined(source.severity)) { + enriched.setSeverity(source.severity); + } + enriched.compliance = source.compliance; + return enriched; + }); + }, [data?.entities, reportOperatingSystems]); + + if (isError) { + return ( +
+ {_('Error while loading Operating Systems for Report {{reportId}}', { + reportId, + })} +
+ ); + } + + const {entitiesCounts: operatingSystemsCounts} = data || {}; + + const displayedFilter = operatingSystemsFilter; + + if (isLoading && !data) { + return ; + } + return ( - - counts={counts} - entities={operatingsystems} - filter={filter} - sortField={sortField} - sortFunctions={operatingssystemsSortFunctions} - sortReverse={sortReverse} + {({ entities, @@ -81,8 +144,8 @@ const OperatingSystemsTab = ({ // @ts-expect-error entities are ReportOperatingSystem[], not Model[] entities={entities} entitiesCounts={entitiesCounts} - filter={filter} - isUpdating={isUpdating} + filter={displayedFilter} + isUpdating={isFetching} sortBy={sortBy} sortDir={sortDir} toggleDetailsIcon={false} @@ -90,11 +153,11 @@ const OperatingSystemsTab = ({ onLastClick={onLastClick} onNextClick={onNextClick} onPreviousClick={onPreviousClick} - onSortChange={onSortChange} + onSortChange={handleSortChange} /> )} ); }; -export default OperatingSystemsTab; +export default OperatingSystemsTabWrapper; diff --git a/src/web/pages/reports/details/OperatingSystemsTable.tsx b/src/web/pages/reports/details/OperatingSystemsTable.tsx index 9be0c88edf..372a8ab348 100644 --- a/src/web/pages/reports/details/OperatingSystemsTable.tsx +++ b/src/web/pages/reports/details/OperatingSystemsTable.tsx @@ -4,11 +4,12 @@ */ import {_, _l} from 'gmp/locale/lang'; -import {COMPLIANCE} from 'gmp/models/compliance'; import type ReportOperatingSystem from 'gmp/models/report/os'; import ComplianceBar from 'web/components/bar/ComplianceBar'; import SeverityBar from 'web/components/bar/SeverityBar'; import OsIcon from 'web/components/icon/OsIcon'; +import IconDivider from 'web/components/layout/IconDivider'; +import Link from 'web/components/link/Link'; import TableData from 'web/components/table/TableData'; import TableHead from 'web/components/table/TableHead'; import TableHeader from 'web/components/table/TableHeader'; @@ -30,7 +31,18 @@ const getColumns = (audit = false) => [ title: _('Operating System'), sortBy: 'name', render: (entity: ReportOperatingSystem) => ( - + + + + + {entity.name} + + + ), align: 'center', }, @@ -38,7 +50,17 @@ const getColumns = (audit = false) => [ key: 'cpe', title: _('CPE'), sortBy: 'cpe', - render: (entity: ReportOperatingSystem) => entity.cpe, + render: (entity: ReportOperatingSystem) => ( + + + {entity.cpe} + + + ), align: 'center', }, { @@ -97,15 +119,7 @@ const Header = ({ title={column.title} width={column.width} onSortChange={onSortChange} - > - {column.render({ - compliance: COMPLIANCE.UNDEFINED, - hosts: {count: 0, hostsByIp: {}, complianceByIp: {}}, - severity: undefined, - cpe: '', - name: '', - } as ReportOperatingSystem)} - + /> ))} diff --git a/src/web/pages/reports/details/__tests__/OperatingSystemsTab.test.jsx b/src/web/pages/reports/details/__tests__/OperatingSystemsTab.test.jsx index 3fa0ecd013..2462a2c42f 100644 --- a/src/web/pages/reports/details/__tests__/OperatingSystemsTab.test.jsx +++ b/src/web/pages/reports/details/__tests__/OperatingSystemsTab.test.jsx @@ -4,45 +4,77 @@ */ import {describe, test, expect, testing} from '@gsa/testing'; -import {screen, rendererWith} from 'web/testing'; +import {screen, rendererWith, waitFor} from 'web/testing'; +import CollectionCounts from 'gmp/collection/collection-counts'; import Filter from 'gmp/models/filter'; +import ReportOperatingSystem from 'gmp/models/report/os'; +import {createSession} from 'gmp/testing'; import {SEVERITY_RATING_CVSS_3} from 'gmp/utils/severity'; import {getMockAuditReport} from 'web/pages/reports/__fixtures__/MockAuditReport'; import {getMockReport} from 'web/pages/reports/__fixtures__/MockReport'; import OperatingSystemsTab from 'web/pages/reports/details/OperatingSystemsTab'; const filter = Filter.fromString( - 'apply_overrides=0 levels=hml rows=2 min_qod=70 first=1 sort-reverse=severity', + 'apply_overrides=0 levels=hml rows=2 min_qod=70 first=1 sort=severity', ); -const gmp = { - settings: { - severityRating: SEVERITY_RATING_CVSS_3, - }, + +// Build API-format OS entities (no severity set, matching get_report_operating_systems response) +const buildApiEntities = () => { + const os1 = ReportOperatingSystem.fromElement({ + best_os_cpe: 'cpe:/foo/bar', + best_os_txt: 'Foo OS', + }); + os1.hosts.count = 2; + + const os2 = ReportOperatingSystem.fromElement({ + best_os_cpe: 'cpe:/lorem/ipsum', + best_os_txt: 'Lorem OS', + }); + os2.hosts.count = 5; + + return [os1, os2]; }; +const createGmp = (apiEntities, responseFilter = filter) => ({ + session: createSession({token: 'test-token'}), + settings: {severityRating: SEVERITY_RATING_CVSS_3}, + reportoperatingsystems: { + get: testing.fn().mockResolvedValue({ + data: apiEntities, + meta: { + filter: responseFilter, + counts: new CollectionCounts({ + all: apiEntities.length, + filtered: apiEntities.length, + first: 1, + length: apiEntities.length, + rows: apiEntities.length, + }), + }, + }), + }, +}); + describe('Report Operating Systems Tab tests', () => { - test('should render Report Operating Systems Tab', () => { + test('should render Report Operating Systems Tab with severity from full report', async () => { const {operatingsystems} = getMockReport(); + const apiEntities = buildApiEntities(); + const gmp = createGmp(apiEntities); - const onSortChange = testing.fn(); - - const {render} = rendererWith({ - gmp, - router: true, - }); + const {render} = rendererWith({gmp, router: true}); const {baseElement} = render( , ); + await waitFor(() => { + expect(screen.getAllByTestId('progressbar-box')).toHaveLength(2); + }); + const images = baseElement.querySelectorAll('img'); const links = baseElement.querySelectorAll('a'); const header = baseElement.querySelectorAll('th'); @@ -56,68 +88,74 @@ describe('Report Operating Systems Tab tests', () => { // Row 1 expect(images[0]).toHaveAttribute('src', '/img/os_unknown.svg'); - expect(links[1]).toHaveAttribute( - 'href', - '/operatingsystems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', - ); expect(links[0]).toHaveTextContent('Foo OS'); expect(links[0]).toHaveAttribute( 'href', '/operatingsystems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', ); expect(links[1]).toHaveTextContent('cpe:/foo/bar'); + expect(links[1]).toHaveAttribute( + 'href', + '/operatingsystems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', + ); expect(bars[0]).toHaveAttribute('title', 'Critical'); expect(bars[0]).toHaveTextContent('10.0 (Critical)'); // Row 2 expect(images[1]).toHaveAttribute('src', '/img/os_unknown.svg'); + expect(links[2]).toHaveTextContent('Lorem OS'); expect(links[2]).toHaveAttribute( 'href', '/operatingsystems?filter=name%3Dcpe%3A%2Florem%2Fipsum', ); - expect(links[2]).toHaveTextContent('Lorem OS'); - expect(links[2]).toHaveAttribute( + expect(links[3]).toHaveTextContent('cpe:/lorem/ipsum'); + expect(links[3]).toHaveAttribute( 'href', '/operatingsystems?filter=name%3Dcpe%3A%2Florem%2Fipsum', ); - expect(links[3]).toHaveTextContent('cpe:/lorem/ipsum'); expect(bars[1]).toHaveAttribute('title', 'Medium'); expect(bars[1]).toHaveTextContent('5.0 (Medium)'); + }); - // Filter - expect(baseElement).toHaveTextContent( - '(Applied filter: apply_overrides=0 levels=hml rows=2 min_qod=70 first=1 sort-reverse=severity)', + test('should show loading state before data arrives', async () => { + const gmp = createGmp(buildApiEntities()); + // Replace with a promise that never resolves to keep the loading state + gmp.reportoperatingsystems.get = testing.fn().mockReturnValue( + new Promise(() => {}), ); + + const {render} = rendererWith({gmp, router: true}); + render(); + + expect(screen.getByTestId('loading')).toBeInTheDocument(); }); }); -const auditfilter = Filter.fromString( +const auditFilter = Filter.fromString( 'apply_overrides=0 levels=hmlg rows=3 min_qod=70 first=1 sort=compliant', ); describe('Audit Report Operating Systems Tab tests', () => { - test('should render Audit Report Operating Systems Tab', () => { + test('should render Audit Report Operating Systems Tab with compliance', async () => { const {operatingsystems} = getMockAuditReport(); + const apiEntities = buildApiEntities(); + const gmp = createGmp(apiEntities, auditFilter); - const onSortChange = testing.fn(); - - const {render} = rendererWith({ - router: true, - }); + const {render} = rendererWith({gmp, router: true}); const {baseElement} = render( , ); + await waitFor(() => { + expect(screen.getAllByTestId('progressbar-box')).toHaveLength(2); + }); + const images = baseElement.querySelectorAll('img'); const links = baseElement.querySelectorAll('a'); const header = baseElement.querySelectorAll('th'); @@ -131,12 +169,8 @@ describe('Audit Report Operating Systems Tab tests', () => { // Row 1 expect(images[0]).toHaveAttribute('src', '/img/os_unknown.svg'); - expect(links[0]).toHaveAttribute( - 'href', - '/operatingsystems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', - ); expect(links[0]).toHaveTextContent('Foo OS'); - expect(links[1]).toHaveAttribute( + expect(links[0]).toHaveAttribute( 'href', '/operatingsystems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', ); @@ -146,10 +180,6 @@ describe('Audit Report Operating Systems Tab tests', () => { // Row 2 expect(images[1]).toHaveAttribute('src', '/img/os_unknown.svg'); - expect(links[2]).toHaveAttribute( - 'href', - '/operatingsystems?filter=name%3Dcpe%3A%2Florem%2Fipsum', - ); expect(links[2]).toHaveTextContent('Lorem OS'); expect(links[2]).toHaveAttribute( 'href', @@ -158,10 +188,5 @@ describe('Audit Report Operating Systems Tab tests', () => { expect(links[3]).toHaveTextContent('cpe:/lorem/ipsum'); expect(bars[1]).toHaveAttribute('title', 'Incomplete'); expect(bars[1]).toHaveTextContent('Incomplete'); - - // Filter - expect(baseElement).toHaveTextContent( - '(Applied filter: apply_overrides=0 levels=hmlg rows=3 min_qod=70 first=1 sort=compliant)', - ); }); }); From 92567ff5c73c7477c137d8add2430223b5ca886e Mon Sep 17 00:00:00 2001 From: daniele-mng Date: Wed, 20 May 2026 11:46:52 +0200 Subject: [PATCH 3/6] remove: report details operating system column severity --- src/gmp/commands/report-operating-system.ts | 5 +- .../reports/details/OperatingSystemsTab.tsx | 9 +- .../reports/details/OperatingSystemsTable.tsx | 14 +-- .../__tests__/OperatingSystemsTab.test.jsx | 18 +-- .../__tests__/OperatingSystemsTable.test.tsx | 116 ++++-------------- 5 files changed, 33 insertions(+), 129 deletions(-) diff --git a/src/gmp/commands/report-operating-system.ts b/src/gmp/commands/report-operating-system.ts index 29c2661bca..2fd35b5370 100644 --- a/src/gmp/commands/report-operating-system.ts +++ b/src/gmp/commands/report-operating-system.ts @@ -26,9 +26,7 @@ interface ReportOperatingSystemsResponseData extends XmlResponseData { get_report_operating_systems?: { get_report_operating_systems_response?: { operating_systems?: { - operating_system?: - | OperatingSystemElement - | OperatingSystemElement[]; + operating_system?: OperatingSystemElement | OperatingSystemElement[]; }; }; }; @@ -90,4 +88,3 @@ class ReportOperatingSystemsCommand extends HttpCommand { } export default ReportOperatingSystemsCommand; - diff --git a/src/web/pages/reports/details/OperatingSystemsTab.tsx b/src/web/pages/reports/details/OperatingSystemsTab.tsx index ec788f63a6..613da3cbf3 100644 --- a/src/web/pages/reports/details/OperatingSystemsTab.tsx +++ b/src/web/pages/reports/details/OperatingSystemsTab.tsx @@ -19,7 +19,7 @@ interface OperatingSystemsTabWrapperProps { audit?: boolean; filter?: Filter; reportId: string; - /** Pre-parsed OS entities from the full report, used to enrich severity and compliance. */ + /** Pre-parsed OS entities from the full report, used to enrich compliance. */ reportOperatingSystems?: ReportOperatingSystem[]; } @@ -33,9 +33,6 @@ type OperatingSystemsSortFunctions = { hosts: ( sortReverse?: boolean, ) => (a: ReportOperatingSystem, b: ReportOperatingSystem) => number; - severity: ( - sortReverse?: boolean, - ) => (a: ReportOperatingSystem, b: ReportOperatingSystem) => number; compliant: ( sortReverse?: boolean, ) => (a: ReportOperatingSystem, b: ReportOperatingSystem) => number; @@ -45,7 +42,6 @@ const operatingSystemsSortFunctions: OperatingSystemsSortFunctions = { name: makeCompareString('name'), cpe: makeCompareString('id'), hosts: makeCompareNumber(entity => entity.hosts.count), - severity: makeCompareNumber('severity', 0), compliant: makeCompareString('compliance'), }; @@ -94,9 +90,6 @@ const OperatingSystemsTabWrapper = ({ best_os_txt: os.name, }); enriched.hosts.count = os.hosts.count; - if (isDefined(source.severity)) { - enriched.setSeverity(source.severity); - } enriched.compliance = source.compliance; return enriched; }); diff --git a/src/web/pages/reports/details/OperatingSystemsTable.tsx b/src/web/pages/reports/details/OperatingSystemsTable.tsx index 372a8ab348..afab37cadb 100644 --- a/src/web/pages/reports/details/OperatingSystemsTable.tsx +++ b/src/web/pages/reports/details/OperatingSystemsTable.tsx @@ -6,7 +6,6 @@ import {_, _l} from 'gmp/locale/lang'; import type ReportOperatingSystem from 'gmp/models/report/os'; import ComplianceBar from 'web/components/bar/ComplianceBar'; -import SeverityBar from 'web/components/bar/SeverityBar'; import OsIcon from 'web/components/icon/OsIcon'; import IconDivider from 'web/components/layout/IconDivider'; import Link from 'web/components/link/Link'; @@ -84,18 +83,7 @@ const getColumns = (audit = false) => [ align: 'center', }, ] - : [ - { - key: 'severity', - title: _('Severity'), - width: '10%', - sortBy: 'severity', - render: (entity: ReportOperatingSystem) => ( - - ), - align: 'center', - }, - ]), + : []), ]; const Header = ({ diff --git a/src/web/pages/reports/details/__tests__/OperatingSystemsTab.test.jsx b/src/web/pages/reports/details/__tests__/OperatingSystemsTab.test.jsx index 2462a2c42f..760e0e79f7 100644 --- a/src/web/pages/reports/details/__tests__/OperatingSystemsTab.test.jsx +++ b/src/web/pages/reports/details/__tests__/OperatingSystemsTab.test.jsx @@ -56,7 +56,7 @@ const createGmp = (apiEntities, responseFilter = filter) => ({ }); describe('Report Operating Systems Tab tests', () => { - test('should render Report Operating Systems Tab with severity from full report', async () => { + test('should render Report Operating Systems Tab', async () => { const {operatingsystems} = getMockReport(); const apiEntities = buildApiEntities(); const gmp = createGmp(apiEntities); @@ -71,20 +71,16 @@ describe('Report Operating Systems Tab tests', () => { />, ); - await waitFor(() => { - expect(screen.getAllByTestId('progressbar-box')).toHaveLength(2); - }); + await screen.findByText('Foo OS'); const images = baseElement.querySelectorAll('img'); const links = baseElement.querySelectorAll('a'); const header = baseElement.querySelectorAll('th'); - const bars = screen.getAllByTestId('progressbar-box'); // Headings expect(header[0]).toHaveTextContent('Operating System'); expect(header[1]).toHaveTextContent('CPE'); expect(header[2]).toHaveTextContent('Hosts'); - expect(header[3]).toHaveTextContent('Severity'); // Row 1 expect(images[0]).toHaveAttribute('src', '/img/os_unknown.svg'); @@ -98,8 +94,6 @@ describe('Report Operating Systems Tab tests', () => { 'href', '/operatingsystems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', ); - expect(bars[0]).toHaveAttribute('title', 'Critical'); - expect(bars[0]).toHaveTextContent('10.0 (Critical)'); // Row 2 expect(images[1]).toHaveAttribute('src', '/img/os_unknown.svg'); @@ -113,16 +107,14 @@ describe('Report Operating Systems Tab tests', () => { 'href', '/operatingsystems?filter=name%3Dcpe%3A%2Florem%2Fipsum', ); - expect(bars[1]).toHaveAttribute('title', 'Medium'); - expect(bars[1]).toHaveTextContent('5.0 (Medium)'); }); test('should show loading state before data arrives', async () => { const gmp = createGmp(buildApiEntities()); // Replace with a promise that never resolves to keep the loading state - gmp.reportoperatingsystems.get = testing.fn().mockReturnValue( - new Promise(() => {}), - ); + gmp.reportoperatingsystems.get = testing + .fn() + .mockReturnValue(new Promise(() => {})); const {render} = rendererWith({gmp, router: true}); render(); diff --git a/src/web/pages/reports/details/__tests__/OperatingSystemsTable.test.tsx b/src/web/pages/reports/details/__tests__/OperatingSystemsTable.test.tsx index dca1d10750..49a2f08d5f 100644 --- a/src/web/pages/reports/details/__tests__/OperatingSystemsTable.test.tsx +++ b/src/web/pages/reports/details/__tests__/OperatingSystemsTable.test.tsx @@ -35,8 +35,8 @@ describe('OperatingSystemsTable', () => { render( , ); @@ -52,10 +52,6 @@ describe('OperatingSystemsTable', () => { expect(columnHeaders.some(th => /CPE/i.exec(th.textContent))).toBe(true); expect(columnHeaders.some(th => /Hosts/i.exec(th.textContent))).toBe(true); - - expect(columnHeaders.some(th => /Severity/i.exec(th.textContent))).toBe( - true, - ); }); test('should render operating system data correctly', () => { @@ -70,8 +66,8 @@ describe('OperatingSystemsTable', () => { render( , ); @@ -81,10 +77,6 @@ describe('OperatingSystemsTable', () => { screen.getByText('Foo OS'); screen.getByText('cpe:/foo/bar'); - - // Should render severity bar - - screen.getByText('10.0 (Critical)'); }); test('should render operating system as link when CPE is defined', () => { @@ -99,8 +91,8 @@ describe('OperatingSystemsTable', () => { render( , ); @@ -126,8 +118,8 @@ describe('OperatingSystemsTable', () => { render( , ); @@ -141,31 +133,6 @@ describe('OperatingSystemsTable', () => { ); }); - test('should render severity bar instead of compliance bar for regular reports', () => { - const {operatingsystems} = getMockReport(); - - const {render} = rendererWith({ - router: true, - - gmp: createGmp(), - }); - - render( - , - ); - - // Should render severity bars, not compliance bars - - screen.getByText('10.0 (Critical)'); - - screen.getByText('5.0 (Medium)'); - }); - test('should render empty state when no operating systems', () => { const counts = new CollectionCounts({ filtered: 0, @@ -207,10 +174,10 @@ describe('OperatingSystemsTable', () => { render( , ); @@ -247,10 +214,10 @@ describe('OperatingSystemsTable', () => { render( , ); @@ -276,8 +243,8 @@ describe('OperatingSystemsTable', () => { render( , @@ -304,8 +271,8 @@ describe('OperatingSystemsTable', () => { render( , @@ -332,8 +299,8 @@ describe('OperatingSystemsTable', () => { render( , @@ -346,11 +313,9 @@ describe('OperatingSystemsTable', () => { expect(onSortChange).toHaveBeenCalledWith('hosts'); }); - test('should handle sorting by severity column', async () => { + test('should not render severity column for regular or audit reports', () => { const {operatingsystems} = getMockReport(); - const onSortChange = testing.fn(); - const {render} = rendererWith({ router: true, @@ -360,38 +325,9 @@ describe('OperatingSystemsTable', () => { render( , - ); - - const severityHeader = await screen.findByText('Severity'); - - await userEvent.click(severityHeader); - - expect(onSortChange).toHaveBeenCalledWith('severity'); - }); - - test('should not render severity column when audit is true', () => { - const {operatingsystems} = getMockAuditReport(); - - const auditFilter = Filter.fromString('first=1 rows=10'); - - const {render} = rendererWith({ - router: true, - - gmp: createGmp(), - }); - - render( - , ); @@ -399,12 +335,10 @@ describe('OperatingSystemsTable', () => { const columnHeaders = within(table).getAllByRole('columnheader'); + expect(columnHeaders).toHaveLength(3); + expect(columnHeaders.some(th => /Severity/i.exec(th.textContent))).toBe( false, ); - - expect(columnHeaders.some(th => /Compliant/i.exec(th.textContent))).toBe( - true, - ); }); }); From f896b3af721a13c67ad8c68f2746463a9746416c Mon Sep 17 00:00:00 2001 From: daniele-mng Date: Wed, 20 May 2026 12:02:17 +0200 Subject: [PATCH 4/6] change: introduce do not reload for completed tasks The following tabs have now do not reload if task is completed: - TLS Certificates - Operating Systems - Error Messages --- src/gmp/commands/report-operating-system.ts | 4 +- ...est.tsx => ReportOperatingSystem.test.tsx} | 0 src/web/hooks/use-query/report-errors.ts | 3 + .../use-query/report-operating-system.ts | 3 + .../use-query/report-tls-certificates.ts | 3 + .../reports/AuditReportDetailsContent.tsx | 10 +++- src/web/pages/reports/DetailsContent.tsx | 4 +- src/web/pages/reports/details/ErrorsTab.tsx | 15 ++++- .../reports/details/OperatingSystemsTab.tsx | 10 ++++ .../reports/details/TlsCertificatesTab.tsx | 10 ++++ .../details/__tests__/ErrorsTab.test.tsx | 60 ++++++++++++++++--- .../__tests__/OperatingSystemsTab.test.jsx | 6 +- .../__tests__/TlsCertificatesTab.test.tsx | 60 ++++++++++++++++++- 13 files changed, 175 insertions(+), 13 deletions(-) rename src/web/hooks/use-query/__tests__/{report-operating-system.test.tsx => ReportOperatingSystem.test.tsx} (100%) diff --git a/src/gmp/commands/report-operating-system.ts b/src/gmp/commands/report-operating-system.ts index 2fd35b5370..64d026b260 100644 --- a/src/gmp/commands/report-operating-system.ts +++ b/src/gmp/commands/report-operating-system.ts @@ -13,6 +13,7 @@ import HttpCommand, { import type Http from 'gmp/http/http'; import type Response from 'gmp/http/response'; import type {XmlResponseData} from 'gmp/http/transform/fast-xml'; +import type {FilterModelElement} from 'gmp/models/filter'; import ReportOperatingSystem from 'gmp/models/report/os'; import {map} from 'gmp/utils/array'; @@ -25,6 +26,7 @@ interface OperatingSystemElement { interface ReportOperatingSystemsResponseData extends XmlResponseData { get_report_operating_systems?: { get_report_operating_systems_response?: { + filters?: FilterModelElement; operating_systems?: { operating_system?: OperatingSystemElement | OperatingSystemElement[]; }; @@ -57,7 +59,7 @@ class ReportOperatingSystemsCommand extends HttpCommand { const data = root.get_report_operating_systems.get_report_operating_systems_response; - const filter = parseFilter(data as any); + const filter = parseFilter(data ?? {}); const entities = map( data?.operating_systems?.operating_system, diff --git a/src/web/hooks/use-query/__tests__/report-operating-system.test.tsx b/src/web/hooks/use-query/__tests__/ReportOperatingSystem.test.tsx similarity index 100% rename from src/web/hooks/use-query/__tests__/report-operating-system.test.tsx rename to src/web/hooks/use-query/__tests__/ReportOperatingSystem.test.tsx diff --git a/src/web/hooks/use-query/report-errors.ts b/src/web/hooks/use-query/report-errors.ts index a2f289475c..05e2ce69a9 100644 --- a/src/web/hooks/use-query/report-errors.ts +++ b/src/web/hooks/use-query/report-errors.ts @@ -10,11 +10,13 @@ import useGetEntities from 'web/queries/useGetEntities'; interface UseGetReportErrorsParams { reportId: string; filter?: Filter; + refetchInterval?: number | false; } export const useGetReportErrors = ({ reportId, filter = undefined, + refetchInterval = undefined, }: UseGetReportErrorsParams) => { const gmp = useGmp(); @@ -28,6 +30,7 @@ export const useGetReportErrors = ({ filter, enabled: Boolean(reportId), keepPreviousData: true, + refetchInterval, }); }; diff --git a/src/web/hooks/use-query/report-operating-system.ts b/src/web/hooks/use-query/report-operating-system.ts index 82394ebfd6..e23815a48a 100644 --- a/src/web/hooks/use-query/report-operating-system.ts +++ b/src/web/hooks/use-query/report-operating-system.ts @@ -11,11 +11,13 @@ import useGetEntities from 'web/queries/useGetEntities'; interface UseGetReportOperatingSystemsParams { reportId: string; filter?: Filter; + refetchInterval?: number | false; } export const useGetReportOperatingSystems = ({ reportId, filter, + refetchInterval = undefined, }: UseGetReportOperatingSystemsParams) => { const gmp = useGmp(); @@ -29,6 +31,7 @@ export const useGetReportOperatingSystems = ({ filter, enabled: Boolean(reportId), keepPreviousData: true, + refetchInterval, }); }; diff --git a/src/web/hooks/use-query/report-tls-certificates.ts b/src/web/hooks/use-query/report-tls-certificates.ts index e1a3d0c0a1..85cb048d73 100644 --- a/src/web/hooks/use-query/report-tls-certificates.ts +++ b/src/web/hooks/use-query/report-tls-certificates.ts @@ -11,11 +11,13 @@ import useGetEntities from 'web/queries/useGetEntities'; interface UseGetReportTlsCertificatesParams { reportId: string; filter?: Filter; + refetchInterval?: number | false; } export const useGetReportTlsCertificates = ({ reportId, filter = undefined, + refetchInterval = undefined, }: UseGetReportTlsCertificatesParams) => { const gmp = useGmp(); @@ -29,6 +31,7 @@ export const useGetReportTlsCertificates = ({ filter, enabled: Boolean(reportId), keepPreviousData: true, + refetchInterval, }); }; diff --git a/src/web/pages/reports/AuditReportDetailsContent.tsx b/src/web/pages/reports/AuditReportDetailsContent.tsx index 2f088be031..6f56686486 100644 --- a/src/web/pages/reports/AuditReportDetailsContent.tsx +++ b/src/web/pages/reports/AuditReportDetailsContent.tsx @@ -305,6 +305,7 @@ const AuditReportDetailsContent = ({ filter={effectiveReportFilter} reportId={reportId} reportOperatingSystems={operatingSystems?.entities} + status={status} />, ), }, @@ -322,6 +323,7 @@ const AuditReportDetailsContent = ({ , ), @@ -329,7 +331,13 @@ const AuditReportDetailsContent = ({ { key: 'errors', title: , - panel: , + panel: ( + + ), }, { key: 'usertags', diff --git a/src/web/pages/reports/DetailsContent.tsx b/src/web/pages/reports/DetailsContent.tsx index f8c1118e49..77d7d519d3 100644 --- a/src/web/pages/reports/DetailsContent.tsx +++ b/src/web/pages/reports/DetailsContent.tsx @@ -388,6 +388,7 @@ const PageContent = ({ filter={activeFilter} reportId={reportId} reportOperatingSystems={operatingsystems?.entities} + status={status} />, ), }, @@ -447,6 +448,7 @@ const PageContent = ({ , ), @@ -457,7 +459,7 @@ const PageContent = ({ ), renderPanel: () => ( - + ), }, { diff --git a/src/web/pages/reports/details/ErrorsTab.tsx b/src/web/pages/reports/details/ErrorsTab.tsx index de9dd46028..a686cc4fea 100644 --- a/src/web/pages/reports/details/ErrorsTab.tsx +++ b/src/web/pages/reports/details/ErrorsTab.tsx @@ -6,9 +6,14 @@ import {useMemo, useState} from 'react'; import {useTranslation} from 'react-i18next'; import Filter from 'gmp/models/filter'; +import {isActive, type TaskStatus} from 'gmp/models/task'; import {isDefined} from 'gmp/utils/identity'; import ErrorPanel from 'web/components/error/ErrorPanel'; import Loading from 'web/components/loading/Loading'; +import { + NO_RELOAD, + USE_DEFAULT_RELOAD_INTERVAL_ACTIVE, +} from 'web/components/loading/Reload'; import useGetReportErrors from 'web/hooks/use-query/report-errors'; import useFilterSortBy from 'web/hooks/useFilterSortBy'; import ErrorsTable from 'web/pages/reports/details/ErrorsTable'; @@ -18,6 +23,7 @@ import {makeCompareIp, makeCompareString} from 'web/utils/Sort'; interface ErrorsTabWrapperProps { filter?: Filter; reportId: string; + status: TaskStatus; } export const errorsSortFunctions = { @@ -28,7 +34,11 @@ export const errorsSortFunctions = { port: makeCompareString('port'), }; -const ErrorsTabWrapper = ({filter, reportId}: ErrorsTabWrapperProps) => { +const ErrorsTabWrapper = ({ + filter, + reportId, + status, +}: ErrorsTabWrapperProps) => { const [_] = useTranslation(); const baseFilter = useMemo(() => { @@ -40,6 +50,9 @@ const ErrorsTabWrapper = ({filter, reportId}: ErrorsTabWrapperProps) => { const {data, isLoading, isFetching, isError, error} = useGetReportErrors({ reportId, filter: errorsFilter, + refetchInterval: isActive(status) + ? USE_DEFAULT_RELOAD_INTERVAL_ACTIVE + : NO_RELOAD, }); const updateFilter = (newFilter: Filter) => { diff --git a/src/web/pages/reports/details/OperatingSystemsTab.tsx b/src/web/pages/reports/details/OperatingSystemsTab.tsx index 613da3cbf3..8a428c1852 100644 --- a/src/web/pages/reports/details/OperatingSystemsTab.tsx +++ b/src/web/pages/reports/details/OperatingSystemsTab.tsx @@ -7,8 +7,13 @@ import {useMemo, useState} from 'react'; import {useTranslation} from 'react-i18next'; import Filter from 'gmp/models/filter'; import ReportOperatingSystem from 'gmp/models/report/os'; +import {isActive, type TaskStatus} from 'gmp/models/task'; import {isDefined} from 'gmp/utils/identity'; import Loading from 'web/components/loading/Loading'; +import { + NO_RELOAD, + USE_DEFAULT_RELOAD_INTERVAL_ACTIVE, +} from 'web/components/loading/Reload'; import useGetReportOperatingSystems from 'web/hooks/use-query/report-operating-system'; import useFilterSortBy from 'web/hooks/useFilterSortBy'; import OperatingSystemsTable from 'web/pages/reports/details/OperatingSystemsTable'; @@ -21,6 +26,7 @@ interface OperatingSystemsTabWrapperProps { reportId: string; /** Pre-parsed OS entities from the full report, used to enrich compliance. */ reportOperatingSystems?: ReportOperatingSystem[]; + status: TaskStatus; } type OperatingSystemsSortFunctions = { @@ -50,6 +56,7 @@ const OperatingSystemsTabWrapper = ({ reportId, audit = false, reportOperatingSystems, + status, }: OperatingSystemsTabWrapperProps) => { const [_] = useTranslation(); @@ -63,6 +70,9 @@ const OperatingSystemsTabWrapper = ({ const {data, isLoading, isFetching, isError} = useGetReportOperatingSystems({ reportId, filter: operatingSystemsFilter, + refetchInterval: isActive(status) + ? USE_DEFAULT_RELOAD_INTERVAL_ACTIVE + : NO_RELOAD, }); const updateFilter = (newFilter: Filter) => { diff --git a/src/web/pages/reports/details/TlsCertificatesTab.tsx b/src/web/pages/reports/details/TlsCertificatesTab.tsx index 516366e400..0775c37698 100644 --- a/src/web/pages/reports/details/TlsCertificatesTab.tsx +++ b/src/web/pages/reports/details/TlsCertificatesTab.tsx @@ -7,8 +7,13 @@ import {useEffect, useMemo, useState} from 'react'; import CollectionCounts from 'gmp/collection/collection-counts'; import Filter from 'gmp/models/filter'; import type ReportTLSCertificate from 'gmp/models/report/tls-certificate'; +import {isActive, type TaskStatus} from 'gmp/models/task'; import ErrorPanel from 'web/components/error/ErrorPanel'; import Loading from 'web/components/loading/Loading'; +import { + NO_RELOAD, + USE_DEFAULT_RELOAD_INTERVAL_ACTIVE, +} from 'web/components/loading/Reload'; import useGetReportTlsCertificates from 'web/hooks/use-query/report-tls-certificates'; import useFilterSortBy from 'web/hooks/useFilterSortBy'; import usePagination from 'web/hooks/usePagination'; @@ -18,12 +23,14 @@ import TLSCertificatesTable from 'web/pages/reports/details/TlsCertificatesTable interface TLSCertificatesTabProps { reportId: string; reportFilter: Filter; + status: TaskStatus; onTlsCertificateDownloadClick: (entity: ReportTLSCertificate) => void; } const TLSCertificatesTab = ({ reportId, reportFilter, + status, onTlsCertificateDownloadClick, }: TLSCertificatesTabProps) => { const [_] = useTranslation(); @@ -44,6 +51,9 @@ const TLSCertificatesTab = ({ useGetReportTlsCertificates({ reportId, filter: tlsCertificatesFilter, + refetchInterval: isActive(status) + ? USE_DEFAULT_RELOAD_INTERVAL_ACTIVE + : NO_RELOAD, }); const updateFilter = (newFilter: Filter) => { diff --git a/src/web/pages/reports/details/__tests__/ErrorsTab.test.tsx b/src/web/pages/reports/details/__tests__/ErrorsTab.test.tsx index 5aa59ca2fd..a185b0fe47 100644 --- a/src/web/pages/reports/details/__tests__/ErrorsTab.test.tsx +++ b/src/web/pages/reports/details/__tests__/ErrorsTab.test.tsx @@ -4,7 +4,7 @@ */ import {describe, expect, test, testing} from '@gsa/testing'; -import {rendererWith, screen, within} from 'web/testing'; +import {act, rendererWith, screen, within} from 'web/testing'; import CollectionCounts from 'gmp/collection/collection-counts'; import Filter from 'gmp/models/filter'; import {createSession} from 'gmp/testing'; @@ -44,13 +44,14 @@ const createGmp = ({ }); const reportId = 'report-123'; +const reloadIntervalActive = 2000; describe('ErrorsTab', () => { test('should render loading state initially', () => { const gmp = createGmp(); const {render} = rendererWith({gmp, router: true, capabilities: true}); - render(); + render(); expect(screen.getByTestId('loading')).toBeInTheDocument(); }); @@ -59,7 +60,7 @@ describe('ErrorsTab', () => { const gmp = createGmp(); const {render} = rendererWith({gmp, router: true, capabilities: true}); - render(); + render(); const table = await screen.findByRole('table'); expect(table).toBeInTheDocument(); @@ -100,7 +101,7 @@ describe('ErrorsTab', () => { }); const {render} = rendererWith({gmp, router: true, capabilities: true}); - render(); + render(); expect(await screen.findByText('No Errors available')).toBeInTheDocument(); }); @@ -113,7 +114,7 @@ describe('ErrorsTab', () => { }); const {render} = rendererWith({gmp, router: true, capabilities: true}); - render(); + render(); expect( await screen.findByText(/Error while loading Errors for Report/), @@ -128,7 +129,7 @@ describe('ErrorsTab', () => { const gmp = createGmp({getReportErrors}); const {render} = rendererWith({gmp, router: true, capabilities: true}); - render(); + render(); await screen.findByRole('table'); @@ -144,10 +145,55 @@ describe('ErrorsTab', () => { const gmp = createGmp(); const {render} = rendererWith({gmp, router: true, capabilities: true}); - render(); + render(); await screen.findByRole('table'); expect(screen.getByText(/Applied filter:/)).toBeInTheDocument(); }); + + describe('Errors polling behavior isActive status', () => { + test.each([ + [ + 'should poll errors when task status is active', + 'Running', + reloadIntervalActive + 50, + 2, + ], + [ + 'should not poll errors when task status is not active', + 'Stopped', + reloadIntervalActive * 10, + 1, + ], + ])('%s', async (_, status, timeToAdvance, expectedCallCount) => { + testing.useFakeTimers(); + + const getReportErrors = testing.fn().mockResolvedValue({ + data: mockErrors, + meta: {filter, counts: mockErrorsCounts}, + }); + const gmp = createGmp({getReportErrors}); + const {render} = rendererWith({gmp, router: true, capabilities: true}); + + render( + , + ); + + await act(async () => {}); + expect(getReportErrors).toHaveBeenCalledTimes(1); + + await act(async () => { + await testing.advanceTimersByTimeAsync(timeToAdvance); + }); + + expect(getReportErrors).toHaveBeenCalledTimes(expectedCallCount); + + testing.useRealTimers(); + }); + }); }); diff --git a/src/web/pages/reports/details/__tests__/OperatingSystemsTab.test.jsx b/src/web/pages/reports/details/__tests__/OperatingSystemsTab.test.jsx index 760e0e79f7..a5982e5e96 100644 --- a/src/web/pages/reports/details/__tests__/OperatingSystemsTab.test.jsx +++ b/src/web/pages/reports/details/__tests__/OperatingSystemsTab.test.jsx @@ -68,6 +68,7 @@ describe('Report Operating Systems Tab tests', () => { filter={filter} reportId="1234" reportOperatingSystems={operatingsystems?.entities} + status="Done" />, ); @@ -117,7 +118,9 @@ describe('Report Operating Systems Tab tests', () => { .mockReturnValue(new Promise(() => {})); const {render} = rendererWith({gmp, router: true}); - render(); + render( + , + ); expect(screen.getByTestId('loading')).toBeInTheDocument(); }); @@ -141,6 +144,7 @@ describe('Audit Report Operating Systems Tab tests', () => { filter={auditFilter} reportId="1234" reportOperatingSystems={operatingsystems?.entities} + status="Done" />, ); diff --git a/src/web/pages/reports/details/__tests__/TlsCertificatesTab.test.tsx b/src/web/pages/reports/details/__tests__/TlsCertificatesTab.test.tsx index 694726b0e6..8d0570a3ff 100644 --- a/src/web/pages/reports/details/__tests__/TlsCertificatesTab.test.tsx +++ b/src/web/pages/reports/details/__tests__/TlsCertificatesTab.test.tsx @@ -4,7 +4,7 @@ */ import {describe, test, expect, testing} from '@gsa/testing'; -import {rendererWith, fireEvent, screen, within} from 'web/testing'; +import {act, rendererWith, fireEvent, screen, within} from 'web/testing'; import CollectionCounts from 'gmp/collection/collection-counts'; import Filter from 'gmp/models/filter'; import {createSession} from 'gmp/testing'; @@ -15,6 +15,7 @@ const filter = Filter.fromString( 'apply_overrides=0 levels=hml rows=3 min_qod=70 first=1 sort-reverse=severity', ); const tlsCertificates = getMockReport().tlsCertificates?.entities ?? []; +const reloadIntervalActive = 2000; const createGmp = ({ getReportTlsCertificates = testing.fn().mockResolvedValue({ @@ -62,6 +63,7 @@ describe('Report TLS Certificates Tab tests', () => { , ); @@ -154,6 +156,7 @@ describe('Report TLS Certificates Tab tests', () => { , ); @@ -170,4 +173,59 @@ describe('Report TLS Certificates Tab tests', () => { tlsCertificates[1], ); }); + + describe('TLS Certificates polling behavior isActive status', () => { + test.each([ + [ + 'should poll TLS certificates when task status is active', + 'Running', + reloadIntervalActive + 50, + 2, + ], + [ + 'should not poll TLS certificates when task status is not active', + 'Stopped', + reloadIntervalActive * 10, + 1, + ], + ])('%s', async (_, status, timeToAdvance, expectedCallCount) => { + testing.useFakeTimers(); + + const getReportTlsCertificates = testing.fn().mockResolvedValue({ + data: tlsCertificates, + meta: { + filter, + counts: new CollectionCounts({ + first: 1, + all: tlsCertificates.length, + filtered: tlsCertificates.length, + length: tlsCertificates.length, + rows: tlsCertificates.length, + }), + }, + }); + const gmp = createGmp({getReportTlsCertificates}); + const {render} = rendererWith({router: true, gmp}); + + render( + , + ); + + await act(async () => {}); + expect(getReportTlsCertificates).toHaveBeenCalledTimes(1); + + await act(async () => { + await testing.advanceTimersByTimeAsync(timeToAdvance); + }); + + expect(getReportTlsCertificates).toHaveBeenCalledTimes(expectedCallCount); + + testing.useRealTimers(); + }); + }); }); From 797bb5d0fa46111ddb9cb37be3cd1fa4ffc8692e Mon Sep 17 00:00:00 2001 From: daniele-mng Date: Wed, 20 May 2026 13:49:50 +0200 Subject: [PATCH 5/6] translations --- public/locales/gsa-de.json | 1 + public/locales/gsa-en.json | 1 + public/locales/gsa-zh_CN.json | 1 + public/locales/gsa-zh_TW.json | 1 + 4 files changed, 4 insertions(+) diff --git a/public/locales/gsa-de.json b/public/locales/gsa-de.json index a66ba0cade..554f72f316 100644 --- a/public/locales/gsa-de.json +++ b/public/locales/gsa-de.json @@ -808,6 +808,7 @@ "Error Messages": "Fehlermeldungen", "Error while loading Container Scanning Results for Report {{reportId}}": "Fehler beim Laden der Container-Scan-Ergebnisse für Bericht {{reportId}}", "Error while loading Errors for Report {{reportId}}": "Fehler beim Laden der Fehler für Bericht {{reportId}}", + "Error while loading Operating Systems for Report {{reportId}}": "Fehler beim Laden der Betriebssysteme für Bericht {{reportId}}", "Error while loading Ports for Report {{reportId}}": "Fehler beim Laden der Ports für Bericht {{reportId}}", "Error while loading Report {{reportId}}": "Fehler beim Laden des Berichts {{reportId}}", "Error while loading Results for Report {{reportId}}": "Fehler beim Laden von Ergebnissen des Berichts {{reportId}}", diff --git a/public/locales/gsa-en.json b/public/locales/gsa-en.json index cea6db39fe..1f9095b7fe 100644 --- a/public/locales/gsa-en.json +++ b/public/locales/gsa-en.json @@ -808,6 +808,7 @@ "Error Messages": "Error Messages", "Error while loading Container Scanning Results for Report {{reportId}}": "", "Error while loading Errors for Report {{reportId}}": "", + "Error while loading Operating Systems for Report {{reportId}}": "", "Error while loading Ports for Report {{reportId}}": "", "Error while loading Report {{reportId}}": "Error while loading Report {{reportId}}", "Error while loading Results for Report {{reportId}}": "Error while loading Results for Report {{reportId}}", diff --git a/public/locales/gsa-zh_CN.json b/public/locales/gsa-zh_CN.json index fb6e55fb4b..8f0ccb60c8 100644 --- a/public/locales/gsa-zh_CN.json +++ b/public/locales/gsa-zh_CN.json @@ -808,6 +808,7 @@ "Error Messages": "错误消息", "Error while loading Container Scanning Results for Report {{reportId}}": "", "Error while loading Errors for Report {{reportId}}": "", + "Error while loading Operating Systems for Report {{reportId}}": "", "Error while loading Ports for Report {{reportId}}": "", "Error while loading Report {{reportId}}": "加载报告 {{reportId}} 时出错", "Error while loading Results for Report {{reportId}}": "加载报告 {{reportId}} 的结果时出错", diff --git a/public/locales/gsa-zh_TW.json b/public/locales/gsa-zh_TW.json index 41bc4511da..bed35b5863 100644 --- a/public/locales/gsa-zh_TW.json +++ b/public/locales/gsa-zh_TW.json @@ -808,6 +808,7 @@ "Error Messages": "錯誤訊息", "Error while loading Container Scanning Results for Report {{reportId}}": "", "Error while loading Errors for Report {{reportId}}": "", + "Error while loading Operating Systems for Report {{reportId}}": "", "Error while loading Ports for Report {{reportId}}": "", "Error while loading Report {{reportId}}": "", "Error while loading Results for Report {{reportId}}": "", From 2e3c5ecf0eb03ba30560ebea667e5de4e09b5636 Mon Sep 17 00:00:00 2001 From: daniele-mng Date: Wed, 20 May 2026 15:02:49 +0200 Subject: [PATCH 6/6] refactor: convert report details OperatingSystemsTab to Typescript --- ....test.jsx => OperatingSystemsTab.test.tsx} | 101 ++++++++++-------- 1 file changed, 57 insertions(+), 44 deletions(-) rename src/web/pages/reports/details/__tests__/{OperatingSystemsTab.test.jsx => OperatingSystemsTab.test.tsx} (63%) diff --git a/src/web/pages/reports/details/__tests__/OperatingSystemsTab.test.jsx b/src/web/pages/reports/details/__tests__/OperatingSystemsTab.test.tsx similarity index 63% rename from src/web/pages/reports/details/__tests__/OperatingSystemsTab.test.jsx rename to src/web/pages/reports/details/__tests__/OperatingSystemsTab.test.tsx index a5982e5e96..043a5abaa5 100644 --- a/src/web/pages/reports/details/__tests__/OperatingSystemsTab.test.jsx +++ b/src/web/pages/reports/details/__tests__/OperatingSystemsTab.test.tsx @@ -4,7 +4,7 @@ */ import {describe, test, expect, testing} from '@gsa/testing'; -import {screen, rendererWith, waitFor} from 'web/testing'; +import {screen, rendererWith, within} from 'web/testing'; import CollectionCounts from 'gmp/collection/collection-counts'; import Filter from 'gmp/models/filter'; import ReportOperatingSystem from 'gmp/models/report/os'; @@ -63,7 +63,7 @@ describe('Report Operating Systems Tab tests', () => { const {render} = rendererWith({gmp, router: true}); - const {baseElement} = render( + render( { />, ); - await screen.findByText('Foo OS'); + // Wait for data to load + const dataCell = await screen.findByText('Foo OS'); + + expect(dataCell).toBeInTheDocument(); - const images = baseElement.querySelectorAll('img'); - const links = baseElement.querySelectorAll('a'); - const header = baseElement.querySelectorAll('th'); + // Batch row lookups + const rows = screen.getAllByRole('row'); - // Headings - expect(header[0]).toHaveTextContent('Operating System'); - expect(header[1]).toHaveTextContent('CPE'); - expect(header[2]).toHaveTextContent('Hosts'); + // Verify headers + expect(rows[0]).toHaveTextContent('Operating System'); + expect(rows[0]).toHaveTextContent('CPE'); + expect(rows[0]).toHaveTextContent('Hosts'); - // Row 1 - expect(images[0]).toHaveAttribute('src', '/img/os_unknown.svg'); - expect(links[0]).toHaveTextContent('Foo OS'); - expect(links[0]).toHaveAttribute( + // Verify Row 1 + const row1Links = within(rows[1]).getAllByRole('link'); + const row1Image = within(rows[1]).getByAltText(''); + + expect(row1Image).toHaveAttribute('src', '/img/os_unknown.svg'); + expect(row1Links[0]).toHaveTextContent('Foo OS'); + expect(row1Links[0]).toHaveAttribute( 'href', '/operatingsystems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', ); - expect(links[1]).toHaveTextContent('cpe:/foo/bar'); - expect(links[1]).toHaveAttribute( + expect(row1Links[1]).toHaveTextContent('cpe:/foo/bar'); + expect(row1Links[1]).toHaveAttribute( 'href', '/operatingsystems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', ); - // Row 2 - expect(images[1]).toHaveAttribute('src', '/img/os_unknown.svg'); - expect(links[2]).toHaveTextContent('Lorem OS'); - expect(links[2]).toHaveAttribute( + // Verify Row 2 + const row2Links = within(rows[2]).getAllByRole('link'); + const row2Image = within(rows[2]).getByAltText(''); + + expect(row2Image).toHaveAttribute('src', '/img/os_unknown.svg'); + expect(row2Links[0]).toHaveTextContent('Lorem OS'); + expect(row2Links[0]).toHaveAttribute( 'href', '/operatingsystems?filter=name%3Dcpe%3A%2Florem%2Fipsum', ); - expect(links[3]).toHaveTextContent('cpe:/lorem/ipsum'); - expect(links[3]).toHaveAttribute( + expect(row2Links[1]).toHaveTextContent('cpe:/lorem/ipsum'); + expect(row2Links[1]).toHaveAttribute( 'href', '/operatingsystems?filter=name%3Dcpe%3A%2Florem%2Fipsum', ); @@ -138,7 +146,7 @@ describe('Audit Report Operating Systems Tab tests', () => { const {render} = rendererWith({gmp, router: true}); - const {baseElement} = render( + render( { />, ); - await waitFor(() => { - expect(screen.getAllByTestId('progressbar-box')).toHaveLength(2); - }); + // Wait for compliance data to render + const progressBars = await screen.findAllByTestId('progressbar-box'); + expect(progressBars).toHaveLength(2); - const images = baseElement.querySelectorAll('img'); - const links = baseElement.querySelectorAll('a'); - const header = baseElement.querySelectorAll('th'); + // Batch row lookups + const rows = screen.getAllByRole('row'); const bars = screen.getAllByTestId('progressbar-box'); - // Headings - expect(header[0]).toHaveTextContent('Operating System'); - expect(header[1]).toHaveTextContent('CPE'); - expect(header[2]).toHaveTextContent('Hosts'); - expect(header[3]).toHaveTextContent('Compliant'); + // Verify headers + expect(rows[0]).toHaveTextContent('Operating System'); + expect(rows[0]).toHaveTextContent('CPE'); + expect(rows[0]).toHaveTextContent('Hosts'); + expect(rows[0]).toHaveTextContent('Compliant'); + + // Verify Row 1 + const row1Links = within(rows[1]).getAllByRole('link'); + const row1Image = within(rows[1]).getByAltText(''); - // Row 1 - expect(images[0]).toHaveAttribute('src', '/img/os_unknown.svg'); - expect(links[0]).toHaveTextContent('Foo OS'); - expect(links[0]).toHaveAttribute( + expect(row1Image).toHaveAttribute('src', '/img/os_unknown.svg'); + expect(row1Links[0]).toHaveTextContent('Foo OS'); + expect(row1Links[0]).toHaveAttribute( 'href', '/operatingsystems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', ); - expect(links[1]).toHaveTextContent('cpe:/foo/bar'); + expect(row1Links[1]).toHaveTextContent('cpe:/foo/bar'); expect(bars[0]).toHaveAttribute('title', 'No'); expect(bars[0]).toHaveTextContent('No'); - // Row 2 - expect(images[1]).toHaveAttribute('src', '/img/os_unknown.svg'); - expect(links[2]).toHaveTextContent('Lorem OS'); - expect(links[2]).toHaveAttribute( + // Verify Row 2 + const row2Links = within(rows[2]).getAllByRole('link'); + const row2Image = within(rows[2]).getByAltText(''); + + expect(row2Image).toHaveAttribute('src', '/img/os_unknown.svg'); + expect(row2Links[0]).toHaveTextContent('Lorem OS'); + expect(row2Links[0]).toHaveAttribute( 'href', '/operatingsystems?filter=name%3Dcpe%3A%2Florem%2Fipsum', ); - expect(links[3]).toHaveTextContent('cpe:/lorem/ipsum'); + expect(row2Links[1]).toHaveTextContent('cpe:/lorem/ipsum'); expect(bars[1]).toHaveAttribute('title', 'Incomplete'); expect(bars[1]).toHaveTextContent('Incomplete'); });