From bc8d8129cd5da80e57268c6d44b3c2e512406f35 Mon Sep 17 00:00:00 2001 From: daniele-mng Date: Mon, 18 May 2026 08:43:31 +0200 Subject: [PATCH 1/5] refactor: report details error table to typescript --- src/web/pages/reports/details/ErrorsTable.jsx | 106 ------ src/web/pages/reports/details/ErrorsTable.tsx | 131 +++++++ .../details/__tests__/ErrorTable.test.tsx | 321 ++++++++++++++++++ 3 files changed, 452 insertions(+), 106 deletions(-) delete mode 100644 src/web/pages/reports/details/ErrorsTable.jsx create mode 100644 src/web/pages/reports/details/ErrorsTable.tsx create mode 100644 src/web/pages/reports/details/__tests__/ErrorTable.test.tsx diff --git a/src/web/pages/reports/details/ErrorsTable.jsx b/src/web/pages/reports/details/ErrorsTable.jsx deleted file mode 100644 index d0aa9b2e22..0000000000 --- a/src/web/pages/reports/details/ErrorsTable.jsx +++ /dev/null @@ -1,106 +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 DetailsLink from 'web/components/link/DetailsLink'; -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 = ({currentSortDir, currentSortBy, sort = true, onSortChange}) => ( - - - - - - - - - -); - -Header.propTypes = { - currentSortBy: PropTypes.string, - currentSortDir: PropTypes.string, - sort: PropTypes.bool, - onSortChange: PropTypes.func, -}; - -const Row = ({entity, links = true}) => { - const {nvt, host, port, description} = entity; - return ( - - {description} - - {isDefined(host.id) ? ( - - - {host.ip} - - - ) : ( - host.ip - )} - - - {host.name} - - - - - {nvt.name} - - - - {port} - - ); -}; - -Row.propTypes = { - entity: PropTypes.object.isRequired, - links: PropTypes.bool, -}; - -export default createEntitiesTable({ - header: Header, - emptyTitle: _l('No Errors available'), - row: Row, -}); diff --git a/src/web/pages/reports/details/ErrorsTable.tsx b/src/web/pages/reports/details/ErrorsTable.tsx new file mode 100644 index 0000000000..f04abe4ce5 --- /dev/null +++ b/src/web/pages/reports/details/ErrorsTable.tsx @@ -0,0 +1,131 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import React from 'react'; +import {_, _l} from 'gmp/locale/lang'; +import type Model from 'gmp/models/model'; +import type {ReportError} from 'gmp/models/report/parser'; +import {isDefined} from 'gmp/utils/identity'; +import DetailsLink from 'web/components/link/DetailsLink'; +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 { + currentSortBy?: string; + currentSortDir?: SortDirectionType; + sort?: boolean; + onSortChange?: (sortBy: string) => void; +} + +interface RowProps { + entity: Model; + links?: boolean; +} + +interface ColumnsProps { + links?: boolean; +} + +const getColumns = ({links = true}: ColumnsProps) => [ + { + key: 'error', + title: _('Error Message'), + sortBy: 'error', + render: (entity: ReportError) => entity.description, + }, + { + key: 'host', + title: _('Host'), + sortBy: 'host', + render: (entity: ReportError) => { + const {host} = entity; + return isDefined(host) && isDefined(host.id) ? ( + + + {host.ip} + + + ) : ( + host?.ip + ); + }, + }, + { + key: 'hostname', + title: _('Hostname'), + sortBy: 'hostname', + render: (entity: ReportError) => {entity.host?.name}, + }, + { + key: 'nvt', + title: _('NVT'), + sortBy: 'nvt', + render: (entity: ReportError) => { + const {nvt} = entity; + return isDefined(nvt.id) ? ( + + + {nvt.name} + + + ) : ( + nvt?.name + ); + }, + }, + { + key: 'port', + title: _('Port'), + sortBy: 'port', + render: (entity: ReportError) => entity.port, + }, +]; + +const Header = ({ + currentSortBy, + currentSortDir, + sort = true, + onSortChange, +}: HeaderProps) => { + const columns = getColumns({}); + return ( + + + {columns.map(column => ( + + ))} + + + ); +}; + +const Row = ({entity, links = true}: RowProps) => { + const errorEntity = entity as unknown as ReportError; + const columns = getColumns({links}); + return ( + + {columns.map(column => ( + {column.render(errorEntity)} + ))} + + ); +}; + +export default createEntitiesTable({ + header: Header, + emptyTitle: _l('No Errors available'), + row: Row, +}); diff --git a/src/web/pages/reports/details/__tests__/ErrorTable.test.tsx b/src/web/pages/reports/details/__tests__/ErrorTable.test.tsx new file mode 100644 index 0000000000..70e4a57d95 --- /dev/null +++ b/src/web/pages/reports/details/__tests__/ErrorTable.test.tsx @@ -0,0 +1,321 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, expect, test, testing} from '@gsa/testing'; +import CollectionCounts from 'gmp/collection/collection-counts'; +import Filter from 'gmp/models/filter'; +import type {ReportError} from 'gmp/models/report/parser'; +import ErrorsTable from 'web/pages/reports/details/ErrorsTable'; +import {rendererWith, screen, within} from 'web/testing'; + +const filter = Filter.fromString('first=1 rows=10'); + +const createMockError = (overrides = {}): ReportError => { + return { + description: 'Test error description', + host: { + id: 'host-123', + ip: '192.168.1.100', + name: 'test-host', + }, + nvt: { + id: '1.3.6.1.4.1.25623.1.1.1.1.1', + name: 'Test NVT', + }, + port: '22', + ...overrides, + }; +}; + +describe('ErrorsTable', () => { + test('should render table with all columns', () => { + const entities = [createMockError()]; + const counts = new CollectionCounts({ + filtered: 1, + all: 1, + first: 1, + rows: 10, + }); + + const gmp = {}; + + const onSortChange = testing.fn(); + const {render} = rendererWith({gmp, capabilities: true, router: true}); + + render( + , + ); + + const table = screen.getByRole('table'); + const columnHeaders = within(table).getAllByRole('columnheader'); + + // Check all expected columns + expect( + columnHeaders.some(th => /Error Message/i.exec(th.textContent)), + ).toBe(true); + expect(columnHeaders.some(th => /Host/i.exec(th.textContent))).toBe(true); + expect(columnHeaders.some(th => /Hostname/i.exec(th.textContent))).toBe( + true, + ); + expect(columnHeaders.some(th => /NVT/i.exec(th.textContent))).toBe(true); + expect(columnHeaders.some(th => /Port/i.exec(th.textContent))).toBe(true); + }); + + test('should render error data correctly', () => { + const entities = [ + createMockError({ + description: 'SSH authentication failed', + }), + ]; + const counts = new CollectionCounts({ + filtered: 1, + all: 1, + first: 1, + rows: 10, + }); + + const gmp = {}; + + const {render} = rendererWith({gmp, capabilities: true, router: true}); + + render( + , + ); + + // Check error description + expect(screen.getByText('SSH authentication failed')).toBeInTheDocument(); + + // Check NVT name + expect(screen.getByText('Test NVT')).toBeInTheDocument(); + + // Check port + expect(screen.getByText('22')).toBeInTheDocument(); + + // Check that hostname is rendered + const table = screen.getByRole('table'); + const allRows = within(table).getAllByRole('row'); + const dataRows = allRows.filter( + row => within(row).queryAllByRole('cell').length > 0, + ); + expect(dataRows).toHaveLength(1); + + const cells = within(dataRows[0]).getAllByRole('cell'); + // Check that hostname is in the cell (it's the 3rd cell - index 2) + const hostnameCell = within(dataRows[0]).getAllByRole('cell')[2]; + expect(hostnameCell?.textContent).toBe('test-host'); + }); + + test('should render host as DetailsLink when host id exists', () => { + const entities = [ + createMockError({ + host: { + id: 'host-456', + ip: '172.16.0.10', + }, + }), + ]; + const counts = new CollectionCounts({ + filtered: 1, + all: 1, + first: 1, + rows: 10, + }); + + const gmp = {}; + + const {render} = rendererWith({gmp, capabilities: true, router: true}); + + render( + , + ); + + const link = screen.getByText('172.16.0.10'); + expect(link).toBeInTheDocument(); + expect(link.closest('a')).toHaveAttribute('href', '/host/host-456'); + }); + + test('should render host as plain text when host id missing', () => { + const entities = [ + createMockError({ + host: { + ip: '172.16.0.20', + }, + }), + ]; + const counts = new CollectionCounts({ + filtered: 1, + all: 1, + first: 1, + rows: 10, + }); + + const gmp = {}; + + const {render} = rendererWith({gmp, capabilities: true, router: true}); + + render( + , + ); + + const link = screen.getByText('172.16.0.20'); + expect(link).toBeInTheDocument(); + // Should not be a link + expect(link.closest('a')).not.toBeInTheDocument(); + }); + + test('should render hostname in italics', () => { + const entities = [ + createMockError({ + host: { + ip: '172.16.0.30', + name: 'my-test-host', + }, + }), + ]; + const counts = new CollectionCounts({ + filtered: 1, + all: 1, + first: 1, + rows: 10, + }); + + const gmp = {}; + + const {render} = rendererWith({gmp, capabilities: true, router: true}); + + render( + , + ); + + // Check that hostname is rendered with italics tag + const table = screen.getByRole('table'); + const allRows = within(table).getAllByRole('row'); + const dataRows = allRows.filter( + row => within(row).queryAllByRole('cell').length > 0, + ); + + // Get the hostname cell (3rd column, index 2) + const hostnameCell = within(dataRows[0]).getAllByRole('cell')[2]; + expect(hostnameCell?.querySelector('i')).toBeInTheDocument(); + expect(hostnameCell?.textContent).toBe('my-test-host'); + }); + + test('should render empty state when no errors', () => { + const counts = new CollectionCounts({ + filtered: 0, + all: 0, + first: 1, + rows: 10, + }); + + const gmp = {}; + + const {render} = rendererWith({gmp, capabilities: true, router: true}); + + render( + , + ); + + expect(screen.getByText('No Errors available')).toBeInTheDocument(); + }); + + test('should render NVT as DetailsLink when nvt id exists', () => { + const entities = [ + createMockError({ + nvt: { + id: '1.3.6.1.4.1.25623.1.1.1.1.1', + name: 'CVE-2024-1234', + }, + }), + ]; + const counts = new CollectionCounts({ + filtered: 1, + all: 1, + first: 1, + rows: 10, + }); + + const gmp = {}; + + const {render} = rendererWith({gmp, capabilities: true, router: true}); + + render( + , + ); + + const link = screen.getByText('CVE-2024-1234'); + expect(link).toBeInTheDocument(); + expect(link.closest('a')).toHaveAttribute( + 'href', + '/nvt/1.3.6.1.4.1.25623.1.1.1.1.1', + ); + }); + + test('should handle sorting by error column', async () => { + const entities = [createMockError()]; + const counts = new CollectionCounts({ + filtered: 1, + all: 1, + first: 1, + rows: 10, + }); + + const gmp = {}; + + const onSortChange = testing.fn(); + const {render} = rendererWith({gmp, capabilities: true, router: true}); + + const {userEvent} = render( + , + ); + + const errorHeader = await screen.findByText('Error Message'); + await userEvent.click(errorHeader); + + expect(onSortChange).toHaveBeenCalledWith('error'); + }); +}); From 34a59733b7fb9bddd5e27d6d687fad91d861e31f Mon Sep 17 00:00:00 2001 From: daniele-mng Date: Mon, 18 May 2026 12:10:26 +0200 Subject: [PATCH 2/5] add: reports-errors to gmp --- .../commands/__tests__/report-errors.test.ts | 315 ++++++++++++++++++ src/gmp/commands/report-errors.ts | 69 ++++ src/gmp/gmp.ts | 4 +- 3 files changed, 387 insertions(+), 1 deletion(-) create mode 100644 src/gmp/commands/__tests__/report-errors.test.ts create mode 100644 src/gmp/commands/report-errors.ts diff --git a/src/gmp/commands/__tests__/report-errors.test.ts b/src/gmp/commands/__tests__/report-errors.test.ts new file mode 100644 index 0000000000..d2fff39acb --- /dev/null +++ b/src/gmp/commands/__tests__/report-errors.test.ts @@ -0,0 +1,315 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, test, expect} from '@gsa/testing'; +import ReportErrorsCommand from 'gmp/commands/report-errors'; +import {createResponse, createHttp} from 'gmp/commands/testing'; + +describe('ReportErrorsCommand tests', () => { + test('should return report errors', async () => { + const response = createResponse({ + get_report_errors: { + get_report_errors_response: { + errors: { + error: [ + { + host: { + ip: '192.168.1.00', + hostname: 'host1.example.com', + }, + port: 443, + nvt: { + _id: 'nvt-1', + name: 'Test NVT 1', + }, + description: 'Error description 1', + }, + { + host: { + ip: '192.168.1.2000', + hostname: 'host2.example.com', + }, + port: 8080, + nvt: { + _id: 'nvt-2', + name: 'Test NVT 2', + }, + description: 'Error description 2', + }, + ], + }, + host: [ + { + _id: 'host-1', + ip: '192.168.1.00', + hostname: 'host1.example.com', + }, + { + _id: 'host-2', + ip: '192.168.1.2000', + hostname: 'host2.example.com', + }, + ], + filters: { + term: 'first=1 rows=100 sort=severity', + filter: { + _id: '', + }, + keywords: { + keyword: [ + {column: 'first', relation: '=', value: '1'}, + {column: 'rows', relation: '=', value: '100'}, + {column: 'sort', relation: '=', value: 'severity'}, + ], + }, + }, + }, + }, + }); + + const fakeHttp = createHttp(response); + const cmd = new ReportErrorsCommand(fakeHttp); + const resp = await cmd.get({report_id: 'r1'}); + + expect(fakeHttp.request).toHaveBeenCalledWith('get', { + args: { + cmd: 'get_report_errors', + details: 1, + report_id: 'r1', + }, + }); + + const {data} = resp; + + expect(data).toHaveLength(2); + expect(data[0]).toBeDefined(); + expect(data[1]).toBeDefined(); + }); + + test('should handle single error element', async () => { + const response = createResponse({ + get_report_errors: { + get_report_errors_response: { + errors: { + error: { + host: { + ip: '10.0.0.00', + hostname: 'single.example.com', + }, + port: 443, + nvt: { + _id: 'nvt-single', + name: 'Single NVT', + }, + description: 'Single error', + }, + }, + host: { + _id: 'host-single', + ip: '10.0.0.00', + hostname: 'single.example.com', + }, + filters: { + term: 'first=1 rows=100', + filter: {_id: ''}, + keywords: { + keyword: [ + {column: 'first', relation: '=', value: '1'}, + {column: 'rows', relation: '=', value: '100'}, + ], + }, + }, + }, + }, + }); + + const fakeHttp = createHttp(response); + const cmd = new ReportErrorsCommand(fakeHttp); + const resp = await cmd.get({report_id: 'r2'}); + + const {data} = resp; + expect(data).toHaveLength(1); + }); + + test('should handle empty errors', async () => { + const response = createResponse({ + get_report_errors: { + get_report_errors_response: { + errors: {}, + host: [], + filters: { + term: 'first=1 rows=100', + filter: {_id: ''}, + keywords: { + keyword: [ + {column: 'first', relation: '=', value: '1'}, + {column: 'rows', relation: '=', value: '100'}, + ], + }, + }, + }, + }, + }); + + const fakeHttp = createHttp(response); + const cmd = new ReportErrorsCommand(fakeHttp); + const resp = await cmd.get({report_id: 'r3'}); + + const {data} = resp; + expect(data).toHaveLength(0); + }); + + test('should throw error for invalid response', async () => { + const response = createResponse({}); + + const fakeHttp = createHttp(response); + const cmd = new ReportErrorsCommand(fakeHttp); + + await expect(cmd.get({report_id: 'r4'})).rejects.toThrow( + 'Invalid response: get_report_errors not found in response', + ); + }); + + test('should pass filter parameter', async () => { + const response = createResponse({ + get_report_errors: { + get_report_errors_response: { + errors: {}, + host: [], + filters: { + term: 'first=1 rows=100', + filter: {_id: ''}, + keywords: { + keyword: [ + {column: 'first', relation: '=', value: '1'}, + {column: 'rows', relation: '=', value: '100'}, + ], + }, + }, + }, + }, + }); + + const fakeHttp = createHttp(response); + const cmd = new ReportErrorsCommand(fakeHttp); + await cmd.get({report_id: 'r5', filter: 'rows=50 first=1'}); + + expect(fakeHttp.request).toHaveBeenCalledWith('get', { + args: { + cmd: 'get_report_errors', + details: 1, + report_id: 'r5', + filter: 'rows=50 first=1', + }, + }); + }); + + test('should handle multiple hosts', async () => { + const response = createResponse({ + get_report_errors: { + get_report_errors_response: { + errors: { + error: [ + { + host: { + ip: '192.168.1.00', + hostname: 'host1.example.com', + }, + port: 443, + nvt: { + _id: 'nvt-1', + name: 'Test NVT 1', + }, + description: 'Error on host1', + }, + { + host: { + ip: '192.168.1.2000', + hostname: 'host2.example.com', + }, + port: 443, + nvt: { + _id: 'nvt-1', + name: 'Test NVT 1', + }, + description: 'Error on host2', + }, + ], + }, + host: [ + { + _id: 'host-1', + ip: '192.168.1.00', + hostname: 'host1.example.com', + }, + { + _id: 'host-2', + ip: '192.168.1.2000', + hostname: 'host2.example.com', + }, + { + _id: 'host-3', + ip: '192.168.1.3000', + hostname: 'host3.example.com', + }, + ], + filters: { + term: 'first=1 rows=100', + filter: {_id: ''}, + keywords: { + keyword: [ + {column: 'first', relation: '=', value: '1'}, + {column: 'rows', relation: '=', value: '100'}, + ], + }, + }, + }, + }, + }); + + const fakeHttp = createHttp(response); + const cmd = new ReportErrorsCommand(fakeHttp); + const resp = await cmd.get({report_id: 'r6'}); + + const {data} = resp; + expect(data).toHaveLength(2); + }); + + test('should include filter in meta', async () => { + const response = createResponse({ + get_report_errors: { + get_report_errors_response: { + errors: { + error: { + host: {ip: '10.0.0.00'}, + port: 443, + nvt: {_id: 'nvt-1'}, + description: 'Error', + }, + }, + host: {ip: '10.0.0.00'}, + filters: { + term: 'rows=50 first=1 sort=ip', + filter: {_id: ''}, + keywords: { + keyword: [ + {column: 'rows', relation: '=', value: '50'}, + {column: 'first', relation: '=', value: '1'}, + {column: 'sort', relation: '=', value: 'ip'}, + ], + }, + }, + }, + }, + }); + + const fakeHttp = createHttp(response); + const cmd = new ReportErrorsCommand(fakeHttp); + const resp = await cmd.get({report_id: 'r7'}); + + const {filter} = resp.meta; + expect(filter).toBeDefined(); + }); +}); diff --git a/src/gmp/commands/report-errors.ts b/src/gmp/commands/report-errors.ts new file mode 100644 index 0000000000..036f21e135 --- /dev/null +++ b/src/gmp/commands/report-errors.ts @@ -0,0 +1,69 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +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 type {FilterModelElement} from 'gmp/models/filter'; +import { + parseErrors, + type ErrorsElement, + type ReportError, + type ReportHostElement, +} from 'gmp/models/report/parser'; + +interface ReportErrorsData { + errors?: ErrorsElement; + host?: ReportHostElement | ReportHostElement[]; + filters?: FilterModelElement; + [key: string]: unknown; +} + +interface ReportErrorsResponseData extends XmlResponseData { + get_report_errors?: { + get_report_errors_response: ReportErrorsData; + }; +} + +class ReportErrorsCommand extends HttpCommand { + constructor(http: Http) { + super(http, {cmd: 'get_report_errors'}); + } + + async get( + params: HttpCommandInputParams = {}, + options?: HttpCommandOptions, + ): Promise> { + const response = await this.httpGetWithTransform( + {details: 1, ...params}, + options, + ); + + const root = response.data as ReportErrorsResponseData; + + if (!root.get_report_errors) { + throw new Error( + 'Invalid response: get_report_errors not found in response', + ); + } + + const data = root.get_report_errors.get_report_errors_response; + const filter = parseFilter(data); + const {entities: errors, counts} = parseErrors(data, filter); + + return response.set(errors, { + filter, + counts, + }); + } +} + +export default ReportErrorsCommand; diff --git a/src/gmp/gmp.ts b/src/gmp/gmp.ts index 84b0afd1d3..53782d6ac1 100644 --- a/src/gmp/gmp.ts +++ b/src/gmp/gmp.ts @@ -14,7 +14,6 @@ import 'gmp/commands/scan-configs'; import 'gmp/commands/schedules'; import 'gmp/commands/tickets'; import 'gmp/commands/tls-certificates'; -import 'gmp/commands/vulns'; import {getCommands} from 'gmp/command'; import AgentCommand from 'gmp/commands/agent'; @@ -59,6 +58,7 @@ import {PortListCommand, PortListsCommand} from 'gmp/commands/port-lists'; import ReportCommand from 'gmp/commands/report'; import ReportConfigCommand from 'gmp/commands/report-config'; 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 ReportPortsCommand from 'gmp/commands/report-ports'; @@ -145,6 +145,7 @@ class Gmp { public readonly report: ReportCommand; public readonly reportconfig: ReportConfigCommand; public readonly reportconfigs: ReportConfigsCommand; + public readonly reporterrors: ReportsErrorsCommand; public readonly reportformat: ReportFormatCommand; public readonly reportformats: ReportFormatsCommand; public readonly reports: ReportsCommand; @@ -235,6 +236,7 @@ class Gmp { this.report = new ReportCommand(this.http); this.reportconfig = new ReportConfigCommand(this.http); this.reportconfigs = new ReportConfigsCommand(this.http); + this.reporterrors = new ReportsErrorsCommand(this.http); this.reportformat = new ReportFormatCommand(this.http); this.reportformats = new ReportFormatsCommand(this.http); this.reports = new ReportsCommand(this.http); From 4484060fbf59d47febd54dc9a2baeaabcd229669 Mon Sep 17 00:00:00 2001 From: daniele-mng Date: Mon, 18 May 2026 16:57:05 +0200 Subject: [PATCH 3/5] add: tanstack query hook for get_report_errors --- src/web/hooks/use-query/report-errors.ts | 34 ++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/web/hooks/use-query/report-errors.ts diff --git a/src/web/hooks/use-query/report-errors.ts b/src/web/hooks/use-query/report-errors.ts new file mode 100644 index 0000000000..a2f289475c --- /dev/null +++ b/src/web/hooks/use-query/report-errors.ts @@ -0,0 +1,34 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type Filter from 'gmp/models/filter'; +import useGmp from 'web/hooks/useGmp'; +import useGetEntities from 'web/queries/useGetEntities'; + +interface UseGetReportErrorsParams { + reportId: string; + filter?: Filter; +} + +export const useGetReportErrors = ({ + reportId, + filter = undefined, +}: UseGetReportErrorsParams) => { + const gmp = useGmp(); + + return useGetEntities({ + gmpMethod: ({filter: reportFilter}) => + gmp.reporterrors.get({ + report_id: reportId, + filter: reportFilter, + }), + queryId: `get_report_errors_${reportId}`, + filter, + enabled: Boolean(reportId), + keepPreviousData: true, + }); +}; + +export default useGetReportErrors; From de7bca95399c65d434eee8b42c4780182c6a2a87 Mon Sep 17 00:00:00 2001 From: daniele-mng Date: Mon, 18 May 2026 18:22:53 +0200 Subject: [PATCH 4/5] refactor: error tab to typescript and use useGetReportErrors --- src/web/pages/reports/details/ErrorsTab.jsx | 76 --------- src/web/pages/reports/details/ErrorsTab.tsx | 143 ++++++++++++++++ .../details/__tests__/ErrorTable.test.tsx | 71 ++++---- .../details/__tests__/ErrorsTab.test.jsx | 73 --------- .../details/__tests__/ErrorsTab.test.tsx | 153 ++++++++++++++++++ 5 files changed, 324 insertions(+), 192 deletions(-) delete mode 100644 src/web/pages/reports/details/ErrorsTab.jsx create mode 100644 src/web/pages/reports/details/ErrorsTab.tsx delete mode 100644 src/web/pages/reports/details/__tests__/ErrorsTab.test.jsx create mode 100644 src/web/pages/reports/details/__tests__/ErrorsTab.test.tsx diff --git a/src/web/pages/reports/details/ErrorsTab.jsx b/src/web/pages/reports/details/ErrorsTab.jsx deleted file mode 100644 index ce208f7503..0000000000 --- a/src/web/pages/reports/details/ErrorsTab.jsx +++ /dev/null @@ -1,76 +0,0 @@ -/* SPDX-FileCopyrightText: 2024 Greenbone AG - * - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import React from 'react'; -import ErrorsTable from 'web/pages/reports/details/ErrorsTable'; -import ReportEntitiesContainer from 'web/pages/reports/details/ReportEntitiesContainer'; -import PropTypes from 'web/utils/PropTypes'; -import {makeCompareIp, makeCompareString} from 'web/utils/Sort'; - -export const errorsSortFunctions = { - error: makeCompareString('description'), - host: makeCompareIp(entity => entity.host.ip), - hostname: makeCompareString(entity => entity.host.name), - nvt: makeCompareString(entity => entity.nvt.name), - port: makeCompareString('port'), -}; - -const ErrorsTab = ({ - counts, - errors, - filter, - isUpdating, - sortField, - sortReverse, - - onSortChange, -}) => ( - - {({ - entities, - entitiesCounts, - sortBy, - sortDir, - onFirstClick, - onLastClick, - onNextClick, - onPreviousClick, - }) => ( - - )} - -); - -ErrorsTab.propTypes = { - counts: PropTypes.object, - errors: PropTypes.array, - filter: PropTypes.filter.isRequired, - isUpdating: PropTypes.bool, - sortField: PropTypes.string.isRequired, - sortReverse: PropTypes.bool.isRequired, - onSortChange: PropTypes.func.isRequired, -}; - -export default ErrorsTab; diff --git a/src/web/pages/reports/details/ErrorsTab.tsx b/src/web/pages/reports/details/ErrorsTab.tsx new file mode 100644 index 0000000000..a3e789fa3e --- /dev/null +++ b/src/web/pages/reports/details/ErrorsTab.tsx @@ -0,0 +1,143 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {useCallback, useMemo, useState} from 'react'; +import {useTranslation} from 'react-i18next'; +import type CollectionCounts from 'gmp/collection/collection-counts'; +import Filter from 'gmp/models/filter'; +import type {ReportError} from 'gmp/models/report/parser'; +import {isDefined} from 'gmp/utils/identity'; +import ErrorPanel from 'web/components/error/ErrorPanel'; +import Loading from 'web/components/loading/Loading'; +import useGetReportErrors from 'web/hooks/use-query/report-errors'; +import useFilterSortBy from 'web/hooks/useFilterSortBy'; +import ErrorsTable from 'web/pages/reports/details/ErrorsTable'; +import ReportEntitiesContainer from 'web/pages/reports/details/ReportEntitiesContainer'; +import {makeCompareIp, makeCompareString} from 'web/utils/Sort'; + +interface ErrorsTabWrapperProps { + filter?: Filter; + reportId: string; + reportErrors?: ReportError[]; + reportErrorsCounts?: CollectionCounts; + onSortChange?: (sortField: string) => void; + onFilterAddLogLevelClick?: () => void; + onFilterDecreaseMinQoDClick?: () => void; + onFilterEditClick?: () => void; + onFilterRemoveClick?: () => void; + onFilterRemoveSeverityClick?: () => void; +} + +export const errorsSortFunctions = { + error: makeCompareString('description'), + host: makeCompareIp(entity => entity.host.ip), + hostname: makeCompareString(entity => entity.host.name), + nvt: makeCompareString(entity => entity.nvt.name), + port: makeCompareString('port'), +}; + +const ErrorsTabWrapper = ({ + filter, + reportId, + reportErrors, + reportErrorsCounts, + onSortChange, + onFilterAddLogLevelClick, + onFilterDecreaseMinQoDClick, + onFilterEditClick, + onFilterRemoveClick, + onFilterRemoveSeverityClick, +}: ErrorsTabWrapperProps) => { + const {t: _} = useTranslation(); + + const baseFilter = useMemo(() => { + return isDefined(filter) ? filter.copy() : new Filter(); + }, [filter]); + + const [errorsFilter, setErrorsFilter] = useState(baseFilter); + + const {data, isLoading, isFetching, isError, error} = useGetReportErrors({ + reportId, + filter: errorsFilter, + }); + + const updateFilter = useCallback((newFilter: Filter) => { + setErrorsFilter(newFilter); + }, []); + + const [sortBy, sortDir, handleSortChange] = useFilterSortBy( + errorsFilter, + updateFilter, + ); + + const handleSort = useCallback( + (newSortBy: string) => { + handleSortChange(newSortBy); + onSortChange?.(newSortBy); + }, + [handleSortChange, onSortChange], + ); + + if (isError) { + return ( + + ); + } + + const errors = data?.entities ?? reportErrors ?? []; + const errorsCounts = data?.entitiesCounts; + + const displayedFilter = errorsFilter; + const finalCounts = errorsCounts || reportErrorsCounts; + + if (isLoading && !data) { + return ; + } + + return ( + + {({ + entities, + entitiesCounts, + sortBy, + sortDir, + onFirstClick, + onLastClick, + onNextClick, + onPreviousClick, + }) => ( + + )} + + ); +}; + +export default ErrorsTabWrapper; diff --git a/src/web/pages/reports/details/__tests__/ErrorTable.test.tsx b/src/web/pages/reports/details/__tests__/ErrorTable.test.tsx index 70e4a57d95..f91897d514 100644 --- a/src/web/pages/reports/details/__tests__/ErrorTable.test.tsx +++ b/src/web/pages/reports/details/__tests__/ErrorTable.test.tsx @@ -4,11 +4,11 @@ */ import {describe, expect, test, testing} from '@gsa/testing'; +import {rendererWith, screen, within} from 'web/testing'; import CollectionCounts from 'gmp/collection/collection-counts'; import Filter from 'gmp/models/filter'; import type {ReportError} from 'gmp/models/report/parser'; import ErrorsTable from 'web/pages/reports/details/ErrorsTable'; -import {rendererWith, screen, within} from 'web/testing'; const filter = Filter.fromString('first=1 rows=10'); @@ -17,7 +17,7 @@ const createMockError = (overrides = {}): ReportError => { description: 'Test error description', host: { id: 'host-123', - ip: '192.168.1.100', + ip: '192.168.1.1000', name: 'test-host', }, nvt: { @@ -47,7 +47,7 @@ describe('ErrorsTable', () => { render( { const table = screen.getByRole('table'); const columnHeaders = within(table).getAllByRole('columnheader'); - // Check all expected columns expect( columnHeaders.some(th => /Error Message/i.exec(th.textContent)), ).toBe(true); @@ -89,32 +88,27 @@ describe('ErrorsTable', () => { render( , ); - // Check error description - expect(screen.getByText('SSH authentication failed')).toBeInTheDocument(); + screen.getByText('SSH authentication failed'); - // Check NVT name - expect(screen.getByText('Test NVT')).toBeInTheDocument(); + screen.getByText('Test NVT'); - // Check port - expect(screen.getByText('22')).toBeInTheDocument(); + screen.getByText('22'); - // Check that hostname is rendered const table = screen.getByRole('table'); - const allRows = within(table).getAllByRole('row'); - const dataRows = allRows.filter( + const rows = within(table).getAllByRole('row'); + const dataRow = rows.find( row => within(row).queryAllByRole('cell').length > 0, ); - expect(dataRows).toHaveLength(1); + expect(dataRow).toBeDefined(); + if (!dataRow) throw new Error('data row not found'); - const cells = within(dataRows[0]).getAllByRole('cell'); - // Check that hostname is in the cell (it's the 3rd cell - index 2) - const hostnameCell = within(dataRows[0]).getAllByRole('cell')[2]; + const hostnameCell = within(dataRow).getAllByRole('cell')[2]; expect(hostnameCell?.textContent).toBe('test-host'); }); @@ -123,7 +117,7 @@ describe('ErrorsTable', () => { createMockError({ host: { id: 'host-456', - ip: '172.16.0.10', + ip: '172.16.0.1000', }, }), ]; @@ -141,14 +135,13 @@ describe('ErrorsTable', () => { render( , ); - const link = screen.getByText('172.16.0.10'); - expect(link).toBeInTheDocument(); + const link = screen.getByText('172.16.0.1000'); expect(link.closest('a')).toHaveAttribute('href', '/host/host-456'); }); @@ -156,7 +149,7 @@ describe('ErrorsTable', () => { const entities = [ createMockError({ host: { - ip: '172.16.0.20', + ip: '172.16.0.2000', }, }), ]; @@ -174,14 +167,13 @@ describe('ErrorsTable', () => { render( , ); - const link = screen.getByText('172.16.0.20'); - expect(link).toBeInTheDocument(); + const link = screen.getByText('172.16.0.2000'); // Should not be a link expect(link.closest('a')).not.toBeInTheDocument(); }); @@ -190,7 +182,7 @@ describe('ErrorsTable', () => { const entities = [ createMockError({ host: { - ip: '172.16.0.30', + ip: '172.16.0.3000', name: 'my-test-host', }, }), @@ -209,21 +201,20 @@ describe('ErrorsTable', () => { render( , ); - // Check that hostname is rendered with italics tag const table = screen.getByRole('table'); - const allRows = within(table).getAllByRole('row'); - const dataRows = allRows.filter( + const rows = within(table).getAllByRole('row'); + const dataRow = rows.find( row => within(row).queryAllByRole('cell').length > 0, ); - // Get the hostname cell (3rd column, index 2) - const hostnameCell = within(dataRows[0]).getAllByRole('cell')[2]; + if (!dataRow) throw new Error('data row not found'); + const hostnameCell = within(dataRow).getAllByRole('cell')[2]; expect(hostnameCell?.querySelector('i')).toBeInTheDocument(); expect(hostnameCell?.textContent).toBe('my-test-host'); }); @@ -241,15 +232,10 @@ describe('ErrorsTable', () => { const {render} = rendererWith({gmp, capabilities: true, router: true}); render( - , + , ); - expect(screen.getByText('No Errors available')).toBeInTheDocument(); + screen.getByText('No Errors available'); }); test('should render NVT as DetailsLink when nvt id exists', () => { @@ -275,14 +261,13 @@ describe('ErrorsTable', () => { render( , ); const link = screen.getByText('CVE-2024-1234'); - expect(link).toBeInTheDocument(); expect(link.closest('a')).toHaveAttribute( 'href', '/nvt/1.3.6.1.4.1.25623.1.1.1.1.1', @@ -306,7 +291,7 @@ describe('ErrorsTable', () => { const {userEvent} = render( { - test('should render Report Errors Tab', () => { - const {errors} = getMockReport(); - - const onSortChange = testing.fn(); - - const {render} = rendererWith({ - capabilities: true, - router: true, - }); - - const {baseElement} = render( - onSortChange('errors', sortField)} - />, - ); - - const header = baseElement.querySelectorAll('th'); - const rows = baseElement.querySelectorAll('tr'); - const links = baseElement.querySelectorAll('a'); - - // Headings - expect(header[0]).toHaveTextContent('Error Message'); - expect(header[1]).toHaveTextContent('Host'); - expect(header[2]).toHaveTextContent('Hostname'); - expect(header[3]).toHaveTextContent('NVT'); - expect(header[4]).toHaveTextContent('Port'); - - // Row 1 - expect(rows[1]).toHaveTextContent('This is an error.'); - expect(links[0]).toHaveAttribute('href', '/host/123'); - expect(links[0]).toHaveTextContent('123.456.78.910'); - expect(rows[1]).toHaveTextContent('foo.bar'); - expect(links[1]).toHaveAttribute('href', '/nvt/314'); - expect(links[1]).toHaveTextContent('NVT1'); - expect(rows[1]).toHaveTextContent('123/tcp'); - - // Row 2 - expect(rows[2]).toHaveTextContent('This is another error'); - expect(links[2]).toHaveAttribute('href', '/host/109'); - expect(links[2]).toHaveTextContent('109.876.54.321'); - expect(rows[2]).toHaveTextContent('lorem.ipsum'); - expect(links[3]).toHaveAttribute('href', '/nvt/159'); - expect(links[3]).toHaveTextContent('NVT2'); - expect(rows[2]).toHaveTextContent('456/tcp'); - - // Filter - expect(baseElement).toHaveTextContent( - '(Applied filter: apply_overrides=0 levels=hml rows=2 min_qod=70 first=1 sort-reverse=severity)', - ); - }); -}); diff --git a/src/web/pages/reports/details/__tests__/ErrorsTab.test.tsx b/src/web/pages/reports/details/__tests__/ErrorsTab.test.tsx new file mode 100644 index 0000000000..09979fdd75 --- /dev/null +++ b/src/web/pages/reports/details/__tests__/ErrorsTab.test.tsx @@ -0,0 +1,153 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, expect, test, testing} from '@gsa/testing'; +import CollectionCounts from 'gmp/collection/collection-counts'; +import Filter from 'gmp/models/filter'; +import {createSession} from 'gmp/testing'; +import {rendererWith, screen, within} from 'web/testing'; +import {getMockReport} from 'web/pages/reports/__fixtures__/MockReport'; +import ErrorsTab from 'web/pages/reports/details/ErrorsTab'; + +const filter = Filter.fromString('first=1 rows=10'); + +const {errors: mockReportErrors} = getMockReport(); +const mockErrors = mockReportErrors?.entities ?? []; +const mockErrorsCounts = + mockReportErrors?.counts ?? + new CollectionCounts({filtered: 0, all: 0, first: 1, rows: 10}); + +const createGmp = ({ + getReportErrors = testing.fn().mockResolvedValue({ + data: mockErrors, + meta: { + filter, + counts: mockErrorsCounts, + }, + }), +} = {}) => ({ + reporterrors: { + get: getReportErrors, + }, + settings: { + reloadInterval: 5000, + reloadIntervalActive: 2000, + reloadIntervalInactive: 10000, + }, + session: createSession({ + timezone: 'CET', + token: 'test-token', + username: 'admin', + }), +}); + +const reportId = 'report-123'; + +describe('ErrorsTab', () => { + test('should render loading state initially', () => { + const gmp = createGmp(); + const {render} = rendererWith({gmp, router: true, capabilities: true}); + + render(); + + expect(screen.getByTestId('loading')).toBeInTheDocument(); + }); + + test('should render table with errors', async () => { + const gmp = createGmp(); + const {render} = rendererWith({gmp, router: true, capabilities: true}); + + render(); + + const table = await screen.findByRole('table'); + expect(table).toBeInTheDocument(); + + const header = within(table).getAllByRole('columnheader'); + expect(header[0]).toHaveTextContent('Error Message'); + expect(header[1]).toHaveTextContent('Host'); + expect(header[2]).toHaveTextContent('Hostname'); + expect(header[3]).toHaveTextContent('NVT'); + expect(header[4]).toHaveTextContent('Port'); + + const rows = within(table).getAllByRole('row'); + expect(rows[1]).toHaveTextContent('This is another error.'); + expect(rows[1]).toHaveTextContent('109.876.54.321'); + expect(rows[1]).toHaveTextContent('NVT2'); + expect(rows[1]).toHaveTextContent('456/tcp'); + + expect(rows[2]).toHaveTextContent('This is an error.'); + expect(rows[2]).toHaveTextContent('123.456.78.910'); + expect(rows[2]).toHaveTextContent('NVT1'); + expect(rows[2]).toHaveTextContent('123/tcp'); + }); + + test('should render empty state when no errors', async () => { + const gmp = createGmp({ + getReportErrors: testing.fn().mockResolvedValue({ + data: [], + meta: { + filter, + counts: new CollectionCounts({ + filtered: 0, + all: 0, + first: 1, + rows: 10, + }), + }, + }), + }); + const {render} = rendererWith({gmp, router: true, capabilities: true}); + + render(); + + expect(await screen.findByText('No Errors available')).toBeInTheDocument(); + }); + + test('should render error panel on fetch failure', async () => { + const gmp = createGmp({ + getReportErrors: testing + .fn() + .mockRejectedValue(new Error('Failed to fetch errors')), + }); + const {render} = rendererWith({gmp, router: true, capabilities: true}); + + render(); + + expect( + await screen.findByText(/Error while loading Errors for Report/), + ).toBeInTheDocument(); + }); + + test('should call API with filter containing report ID', async () => { + 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 screen.findByRole('table'); + + expect(getReportErrors).toHaveBeenCalledWith( + expect.objectContaining({ + report_id: reportId, + filter: expect.objectContaining({}), + }), + ); + }); + + test('should show applied filter', async () => { + const gmp = createGmp(); + const {render} = rendererWith({gmp, router: true, capabilities: true}); + + render(); + + await screen.findByRole('table'); + + expect(screen.getByText(/Applied filter:/)).toBeInTheDocument(); + }); +}); From fd6e1d5a90114f8230774df1bca0787e5a2775e9 Mon Sep 17 00:00:00 2001 From: daniele-mng Date: Mon, 18 May 2026 18:25:40 +0200 Subject: [PATCH 5/5] adjust files and test to use refactored report error tab --- 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 + .../reports/AuditReportDetailsContent.tsx | 15 +----- src/web/pages/reports/DetailsContent.tsx | 11 +---- .../AuditReportDetailsContent.test.tsx | 49 ++++++++++++------- .../__tests__/AuditReportDetailsPage.test.tsx | 35 ++++++++----- .../reports/__tests__/DetailsContent.test.tsx | 39 ++++++++++++++- .../reports/__tests__/DetailsPage.test.tsx | 35 ++++++++----- src/web/pages/reports/details/ErrorsTab.tsx | 47 +++--------------- .../details/__tests__/ErrorsTab.test.tsx | 2 +- 12 files changed, 129 insertions(+), 108 deletions(-) diff --git a/public/locales/gsa-de.json b/public/locales/gsa-de.json index 4399b62ed5..a66ba0cade 100644 --- a/public/locales/gsa-de.json +++ b/public/locales/gsa-de.json @@ -807,6 +807,7 @@ "Error Message": "Fehlermeldung", "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 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 3c274baff3..cea6db39fe 100644 --- a/public/locales/gsa-en.json +++ b/public/locales/gsa-en.json @@ -807,6 +807,7 @@ "Error Message": "Error Message", "Error Messages": "Error Messages", "Error while loading Container Scanning Results for Report {{reportId}}": "", + "Error while loading Errors 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 ffdb68ef1d..fb6e55fb4b 100644 --- a/public/locales/gsa-zh_CN.json +++ b/public/locales/gsa-zh_CN.json @@ -807,6 +807,7 @@ "Error Message": "错误消息", "Error Messages": "错误消息", "Error while loading Container Scanning Results for Report {{reportId}}": "", + "Error while loading Errors 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 194cbaf926..41bc4511da 100644 --- a/public/locales/gsa-zh_TW.json +++ b/public/locales/gsa-zh_TW.json @@ -807,6 +807,7 @@ "Error Message": "錯誤訊息", "Error Messages": "錯誤訊息", "Error while loading Container Scanning Results for Report {{reportId}}": "", + "Error while loading Errors for Report {{reportId}}": "", "Error while loading Ports for Report {{reportId}}": "", "Error while loading Report {{reportId}}": "", "Error while loading Results for Report {{reportId}}": "", diff --git a/src/web/pages/reports/AuditReportDetailsContent.tsx b/src/web/pages/reports/AuditReportDetailsContent.tsx index abff84dfb1..dc4a3d8f68 100644 --- a/src/web/pages/reports/AuditReportDetailsContent.tsx +++ b/src/web/pages/reports/AuditReportDetailsContent.tsx @@ -176,7 +176,6 @@ const AuditReportDetailsContent = ({ const gmp = useGmp(); const [_] = useTranslation(); - const errors = report?.errors; const hosts = report?.hosts; const operatingSystems = report?.operatingSystems; const results = report?.results; @@ -334,19 +333,7 @@ const AuditReportDetailsContent = ({ { key: 'errors', title: , - panel: ( - - onSortChange('errors', sortField) - } - /> - ), + panel: , }, { key: 'usertags', diff --git a/src/web/pages/reports/DetailsContent.tsx b/src/web/pages/reports/DetailsContent.tsx index 4a62b436ee..98ab9330ca 100644 --- a/src/web/pages/reports/DetailsContent.tsx +++ b/src/web/pages/reports/DetailsContent.tsx @@ -215,7 +215,6 @@ const PageContent = ({ applications, closedCves, cves, - errors, hosts, operatingsystems, results, @@ -462,15 +461,7 @@ const PageContent = ({ ), renderPanel: () => ( - onSortChange('errors', sortField)} - /> + ), }, { diff --git a/src/web/pages/reports/__tests__/AuditReportDetailsContent.test.tsx b/src/web/pages/reports/__tests__/AuditReportDetailsContent.test.tsx index dc9dc63690..0fcdd042b4 100644 --- a/src/web/pages/reports/__tests__/AuditReportDetailsContent.test.tsx +++ b/src/web/pages/reports/__tests__/AuditReportDetailsContent.test.tsx @@ -33,9 +33,37 @@ const createGmp = ({reportResultsThreshold = 10} = {}) => ({ reportResultsThreshold, }, results: { - get: testing.fn(), + get: testing.fn().mockResolvedValue({ + data: [], + meta: { + filter, + counts: new CollectionCounts({filtered: 0, all: 0, first: 1, rows: 10}), + }, + }), }, - session: createSession({timezone: 'CET'}), + reporterrors: { + get: testing.fn().mockResolvedValue({ + data: getMockAuditReport().errors?.entities ?? [], + meta: { + filter, + counts: new CollectionCounts({filtered: 2, all: 2, first: 1, rows: 10}), + }, + }), + }, + reporttlscertificates: { + get: testing.fn().mockResolvedValue({ + data: [], + meta: { + filter, + counts: new CollectionCounts({filtered: 0, all: 0, first: 1, rows: 10}), + }, + }), + }, + session: createSession({ + timezone: 'CET', + token: 'test-token', + username: 'admin', + }), user: { currentSettings: testing.fn().mockResolvedValue({foo: 'bar'}), getReportComposerDefaults: testing.fn().mockResolvedValue({foo: 'bar'}), @@ -374,8 +402,8 @@ describe('AuditReportDetailsContent tests', () => { ); expect( - screen.getByRole('columnheader', {name: /Error Message/i}), - ).toBeInTheDocument(); + screen.queryByRole('row', {name: /^Task Name/}), + ).not.toBeInTheDocument(); }); test('should switch to User Tags tab', () => { @@ -434,19 +462,6 @@ describe('AuditReportDetailsContent tests', () => { }); describe('Sorting', () => { - test('should call onSortChange when Error Messages column header is clicked', () => { - const cbs = renderContent(); - - const tablist = screen.getByRole('tablist'); - fireEvent.click( - within(tablist).getByRole('tab', {name: /^error messages/i}), - ); - - fireEvent.click(screen.getByTestId('table-header-sort-by-error')); - - expect(cbs.onSortChange).toHaveBeenCalledWith('errors', 'error'); - }); - test('should call onSortChange when Hosts IP column header is clicked', () => { const cbs = renderContent(); diff --git a/src/web/pages/reports/__tests__/AuditReportDetailsPage.test.tsx b/src/web/pages/reports/__tests__/AuditReportDetailsPage.test.tsx index ecc8efb7db..7efb430894 100644 --- a/src/web/pages/reports/__tests__/AuditReportDetailsPage.test.tsx +++ b/src/web/pages/reports/__tests__/AuditReportDetailsPage.test.tsx @@ -92,6 +92,15 @@ const createGmp = () => ({ removeAssets: testing.fn().mockResolvedValue({}), download: testing.fn().mockResolvedValue({data: 'report-blob-data'}), }, + reporterrors: { + get: testing.fn().mockResolvedValue({ + data: entity.report?.errors?.entities ?? [], + meta: { + filter: Filter.fromString(''), + counts: entity.report?.errors?.counts ?? new CollectionCounts(), + }, + }), + }, reporttlscertificates: { get: testing.fn().mockResolvedValue({ data: [], @@ -435,15 +444,15 @@ describe('AuditReportDetailsPage', () => { await screen.findByRole('tab', {name: /^Error Messages/}); fireEvent.click(screen.getByRole('tab', {name: /^Error Messages/})); - // Click on the "Error Message" column header to sort - const errorMessageHeader = await screen.findByRole('columnheader', { - name: /Error Message/, - }); - fireEvent.click(errorMessageHeader); + // Click on the sort button for the "Error Message" column + const sortButton = await screen.findByTestId( + 'table-header-sort-by-error', + ); + fireEvent.click(sortButton); - // The column header should still be in the document after the sort + // The sort button should still be in the document after sorting expect( - screen.getByRole('columnheader', {name: /Error Message/}), + screen.getByTestId('table-header-sort-by-error'), ).toBeInTheDocument(); }); @@ -454,17 +463,17 @@ describe('AuditReportDetailsPage', () => { await screen.findByRole('tab', {name: /^Error Messages/}); fireEvent.click(screen.getByRole('tab', {name: /^Error Messages/})); - const errorMessageHeader = await screen.findByRole('columnheader', { - name: /Error Message/, - }); + const sortButton = await screen.findByTestId( + 'table-header-sort-by-error', + ); // Click once to sort ascending - fireEvent.click(errorMessageHeader); + fireEvent.click(sortButton); // Click again to toggle to descending - fireEvent.click(errorMessageHeader); + fireEvent.click(sortButton); expect( - screen.getByRole('columnheader', {name: /Error Message/}), + screen.getByTestId('table-header-sort-by-error'), ).toBeInTheDocument(); }); }); diff --git a/src/web/pages/reports/__tests__/DetailsContent.test.tsx b/src/web/pages/reports/__tests__/DetailsContent.test.tsx index 145ba075f4..1c098afdf1 100644 --- a/src/web/pages/reports/__tests__/DetailsContent.test.tsx +++ b/src/web/pages/reports/__tests__/DetailsContent.test.tsx @@ -12,6 +12,8 @@ import {createSession} from 'gmp/testing'; import {getMockReport} from 'web/pages/reports/__fixtures__/MockReport'; import DetailsContent from 'web/pages/reports/DetailsContent'; +const mockReport = getMockReport(); + const filter = Filter.fromString( 'apply_overrides=0 levels=hml rows=2 min_qod=70 first=1 sort-reverse=severity', ); @@ -43,6 +45,35 @@ const createGmp = ({reportResultsThreshold = 10} = {}) => ({ manualUrl, reportResultsThreshold, }, + reporterrors: { + get: testing.fn().mockResolvedValue({ + data: mockReport.errors?.entities ?? [], + meta: { + filter: Filter.fromString('rows=10'), + counts: + mockReport.errors?.counts ?? + new CollectionCounts({filtered: 0, all: 0}), + }, + }), + }, + reportports: { + get: testing.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(''), + counts: new CollectionCounts({filtered: 0, all: 0}), + }, + }), + }, + reporttlscertificates: { + get: testing.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(''), + counts: new CollectionCounts({filtered: 0, all: 0}), + }, + }), + }, session: createSession({ token: 'test-token', username: 'admin', @@ -59,7 +90,13 @@ const createGmp = ({reportResultsThreshold = 10} = {}) => ({ getReportComposerDefaults: testing.fn().mockResolvedValue({foo: 'bar'}), }, results: { - get: testing.fn().mockResolvedValue({data: []}), + get: testing.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(''), + counts: new CollectionCounts({filtered: 0, all: 0}), + }, + }), }, }); diff --git a/src/web/pages/reports/__tests__/DetailsPage.test.tsx b/src/web/pages/reports/__tests__/DetailsPage.test.tsx index 64fd9ceb7b..1e944ed079 100644 --- a/src/web/pages/reports/__tests__/DetailsPage.test.tsx +++ b/src/web/pages/reports/__tests__/DetailsPage.test.tsx @@ -91,6 +91,15 @@ const createGmp = () => ({ removeAssets: testing.fn().mockResolvedValue({}), download: testing.fn().mockResolvedValue({data: 'report-blob-data'}), }, + reporterrors: { + get: testing.fn().mockResolvedValue({ + data: entity.report?.errors?.entities ?? [], + meta: { + filter: Filter.fromString(''), + counts: entity.report?.errors?.counts ?? new CollectionCounts(), + }, + }), + }, reporttlscertificates: { get: testing.fn().mockResolvedValue({ data: [], @@ -444,15 +453,15 @@ describe('DetailsPage', () => { await screen.findByRole('tab', {name: /^Error Messages/}); fireEvent.click(screen.getByRole('tab', {name: /^Error Messages/})); - // Click on the "Error Message" column header to sort - const errorMessageHeader = await screen.findByRole('columnheader', { - name: /Error Message/, - }); - fireEvent.click(errorMessageHeader); + // Click on the sort button for the "Error Message" column + const sortButton = await screen.findByTestId( + 'table-header-sort-by-error', + ); + fireEvent.click(sortButton); - // The column header should still be in the document after the sort + // The sort button should still be in the document after sorting expect( - screen.getByRole('columnheader', {name: /Error Message/}), + screen.getByTestId('table-header-sort-by-error'), ).toBeInTheDocument(); }); @@ -463,17 +472,17 @@ describe('DetailsPage', () => { await screen.findByRole('tab', {name: /^Error Messages/}); fireEvent.click(screen.getByRole('tab', {name: /^Error Messages/})); - const errorMessageHeader = await screen.findByRole('columnheader', { - name: /Error Message/, - }); + const sortButton = await screen.findByTestId( + 'table-header-sort-by-error', + ); // Click once to sort ascending - fireEvent.click(errorMessageHeader); + fireEvent.click(sortButton); // Click again to toggle to descending - fireEvent.click(errorMessageHeader); + fireEvent.click(sortButton); expect( - screen.getByRole('columnheader', {name: /Error Message/}), + screen.getByTestId('table-header-sort-by-error'), ).toBeInTheDocument(); }); }); diff --git a/src/web/pages/reports/details/ErrorsTab.tsx b/src/web/pages/reports/details/ErrorsTab.tsx index a3e789fa3e..de9dd46028 100644 --- a/src/web/pages/reports/details/ErrorsTab.tsx +++ b/src/web/pages/reports/details/ErrorsTab.tsx @@ -3,11 +3,9 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import {useCallback, useMemo, useState} from 'react'; +import {useMemo, useState} from 'react'; import {useTranslation} from 'react-i18next'; -import type CollectionCounts from 'gmp/collection/collection-counts'; import Filter from 'gmp/models/filter'; -import type {ReportError} from 'gmp/models/report/parser'; import {isDefined} from 'gmp/utils/identity'; import ErrorPanel from 'web/components/error/ErrorPanel'; import Loading from 'web/components/loading/Loading'; @@ -20,14 +18,6 @@ import {makeCompareIp, makeCompareString} from 'web/utils/Sort'; interface ErrorsTabWrapperProps { filter?: Filter; reportId: string; - reportErrors?: ReportError[]; - reportErrorsCounts?: CollectionCounts; - onSortChange?: (sortField: string) => void; - onFilterAddLogLevelClick?: () => void; - onFilterDecreaseMinQoDClick?: () => void; - onFilterEditClick?: () => void; - onFilterRemoveClick?: () => void; - onFilterRemoveSeverityClick?: () => void; } export const errorsSortFunctions = { @@ -38,19 +28,8 @@ export const errorsSortFunctions = { port: makeCompareString('port'), }; -const ErrorsTabWrapper = ({ - filter, - reportId, - reportErrors, - reportErrorsCounts, - onSortChange, - onFilterAddLogLevelClick, - onFilterDecreaseMinQoDClick, - onFilterEditClick, - onFilterRemoveClick, - onFilterRemoveSeverityClick, -}: ErrorsTabWrapperProps) => { - const {t: _} = useTranslation(); +const ErrorsTabWrapper = ({filter, reportId}: ErrorsTabWrapperProps) => { + const [_] = useTranslation(); const baseFilter = useMemo(() => { return isDefined(filter) ? filter.copy() : new Filter(); @@ -63,23 +42,15 @@ const ErrorsTabWrapper = ({ filter: errorsFilter, }); - const updateFilter = useCallback((newFilter: Filter) => { + const updateFilter = (newFilter: Filter) => { setErrorsFilter(newFilter); - }, []); + }; const [sortBy, sortDir, handleSortChange] = useFilterSortBy( errorsFilter, updateFilter, ); - const handleSort = useCallback( - (newSortBy: string) => { - handleSortChange(newSortBy); - onSortChange?.(newSortBy); - }, - [handleSortChange, onSortChange], - ); - if (isError) { return ( ; @@ -103,7 +72,7 @@ const ErrorsTabWrapper = ({ return ( )} diff --git a/src/web/pages/reports/details/__tests__/ErrorsTab.test.tsx b/src/web/pages/reports/details/__tests__/ErrorsTab.test.tsx index 09979fdd75..5aa59ca2fd 100644 --- a/src/web/pages/reports/details/__tests__/ErrorsTab.test.tsx +++ b/src/web/pages/reports/details/__tests__/ErrorsTab.test.tsx @@ -4,10 +4,10 @@ */ import {describe, expect, test, testing} from '@gsa/testing'; +import {rendererWith, screen, within} from 'web/testing'; import CollectionCounts from 'gmp/collection/collection-counts'; import Filter from 'gmp/models/filter'; import {createSession} from 'gmp/testing'; -import {rendererWith, screen, within} from 'web/testing'; import {getMockReport} from 'web/pages/reports/__fixtures__/MockReport'; import ErrorsTab from 'web/pages/reports/details/ErrorsTab';