diff --git a/e2e/navigation/legacy-route-redirects.spec.ts b/e2e/navigation/legacy-route-redirects.spec.ts new file mode 100644 index 0000000000..b72a91f171 --- /dev/null +++ b/e2e/navigation/legacy-route-redirects.spec.ts @@ -0,0 +1,45 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {login} from 'e2e/credentials'; +import {expect, test} from 'e2e/fixtures'; + +const compatibilityRoutes = [ + {legacy: '/auditreports', canonical: '/audit-reports'}, + {legacy: '/operatingsystems', canonical: '/operating-systems'}, + {legacy: '/scanconfigs', canonical: '/scan-configs'}, +]; + +const canonicalRoutes = compatibilityRoutes.map(({canonical}) => canonical); + +test.describe('canonical routes', () => { + test.beforeEach(async ({page}) => { + await login(page); + }); + + for (const route of canonicalRoutes) { + test(`${route} stays on its canonical URL`, async ({page}) => { + await page.goto(route); + + await expect(page).toHaveURL(new RegExp(`${route}$`)); + await expect(page).not.toHaveURL(/\/login(?:$|\?)/); + }); + } +}); + +test.describe('legacy route redirects', () => { + test.beforeEach(async ({page}) => { + await login(page); + }); + + for (const {legacy, canonical} of compatibilityRoutes) { + test(`${legacy} redirects to ${canonical}`, async ({page}) => { + await page.goto(legacy); + + await expect(page).toHaveURL(new RegExp(`${canonical}$`)); + await expect(page).not.toHaveURL(/\/login(?:$|\?)/); + }); + } +}); diff --git a/e2e/navigation/menu-links.spec.ts b/e2e/navigation/menu-links.spec.ts index 83875e38dc..da4beb0aad 100644 --- a/e2e/navigation/menu-links.spec.ts +++ b/e2e/navigation/menu-links.spec.ts @@ -35,7 +35,10 @@ const expandSidebarSections = async (page: Page) => { for (const label of topLevelMenuLabels) { const section = page.getByRole('link', {name: label, exact: true}).first(); if ((await section.count()) > 0) { - await section.click(); + // Top-level menu entries are placeholder links. Their parent owns the + // expand action, while clicking the link itself navigates and remounts + // the menu before its expanded state can persist. + await section.locator('..').click(); } } }; diff --git a/e2e/users/users-helpers.ts b/e2e/users/users-helpers.ts index 081906116b..0583e52dfd 100644 --- a/e2e/users/users-helpers.ts +++ b/e2e/users/users-helpers.ts @@ -166,6 +166,7 @@ const selectMultipleItems = async ( page: Page, groupTitle: string, numberOfItems: number, + expectedLabels?: string[], ) => { const formGroup = page .locator('[data-testid="form-group"]') @@ -174,14 +175,16 @@ const selectMultipleItems = async ( const input = formGroup.locator('[data-testid="multi-select"]').first(); await expect(input).toBeVisible(); + await expect(input).toBeEnabled(); await input.click(); const options = page.getByRole('option'); - await expect(options.nth(numberOfItems - 1)).toBeVisible(); - - const labels: string[] = []; - for (let index = 0; index < numberOfItems; index += 1) { - labels.push(await options.nth(index).innerText()); + const labels = expectedLabels ?? []; + if (labels.length === 0) { + await expect(options.nth(numberOfItems - 1)).toBeVisible(); + for (let index = 0; index < numberOfItems; index += 1) { + labels.push(await options.nth(index).innerText()); + } } for (const label of labels) { diff --git a/e2e/users/users-list.spec.ts b/e2e/users/users-list.spec.ts index 53c30b5af3..374a87d62c 100644 --- a/e2e/users/users-list.spec.ts +++ b/e2e/users/users-list.spec.ts @@ -108,7 +108,24 @@ test.describe('users page flows', () => { page, }) => { const userName = createUniqueUserName('e2e-multiple-assignments'); + const groupNames = [ + createUniqueUserName('e2e-user-group'), + createUniqueUserName('e2e-user-group'), + ]; + await page.goto('/groups'); + await expect(page).not.toHaveURL(/\/login(?:$|\?)/); + for (const groupName of groupNames) { + await page.getByTitle('New Group').click(); + await page.locator('input[name="name"]').fill(groupName); + await page.getByTestId('dialog-save-button').click(); + await expect(page.getByRole('dialog')).toHaveCount(0); + await expect( + page.getByRole('row', {name: new RegExp(groupName)}), + ).toBeVisible(); + } + + await gotoUsersPage(page); await openListCreateDialog(page); await createUserFromCurrentPage(page, userName); @@ -117,7 +134,12 @@ test.describe('users page flows', () => { await expect(page.locator('input[name="name"]')).toBeVisible(); const roleLabels = await selectMultipleItems(page, 'Roles', 2); - const groupLabels = await selectMultipleItems(page, 'Groups', 2); + const groupLabels = await selectMultipleItems( + page, + 'Groups', + groupNames.length, + groupNames, + ); await saveUserDialog(page); await applyFilter(page, `name=${userName}`); @@ -141,6 +163,14 @@ test.describe('users page flows', () => { await closeTopDialog(page); await deleteSingleUserFromList(page, userName); + await page.goto('/groups'); + for (const groupName of groupNames) { + const groupRow = page + .getByRole('row', {name: new RegExp(groupName)}) + .first(); + await groupRow.getByTitle('Move Group to trashcan').click(); + await expect(groupRow).toHaveCount(0); + } await resetFilter(page); }); diff --git a/src/gmp/utils/entity-type.ts b/src/gmp/utils/entity-type.ts index 6697480b78..0f634fc3d1 100644 --- a/src/gmp/utils/entity-type.ts +++ b/src/gmp/utils/entity-type.ts @@ -6,6 +6,7 @@ import {_l, _} from 'gmp/locale/lang'; import logger from 'gmp/log'; import {isDefined} from 'gmp/utils/identity'; +import {entityURL as entityRouteURL} from 'web/route-paths'; export interface WithEntityType { entityType: EntityType; @@ -245,24 +246,5 @@ export const resourceType = (type?: EntityType): string | undefined => { return apiType(type); }; -const ENTITY_URLS = { - agentgroup: 'agent-group', - agentinstaller: 'agent-installer', - auditreport: 'audit-report', - certbund: 'cert-bund-advisory', - dfncert: 'dfn-cert-advisory', - ociimagetarget: 'oci-image-target', - operatingsystem: 'operating-system', - webapplicationtarget: 'web-application-target', - portlist: 'port-list', - portrange: 'port-range', - reportconfig: 'report-config', - reportformat: 'report-format', - scanconfig: 'scan-config', - tlscertificate: 'tls-certificate', -} as Record; - -export const entityURL = (type: EntityType, id: string): string => { - const urlType = ENTITY_URLS[type] ?? type; - return `/${urlType}/${encodeURIComponent(id)}`; -}; +export const entityURL = (type: EntityType, id: string): string => + entityRouteURL(type, id); diff --git a/src/web/Authorized.tsx b/src/web/Authorized.tsx index 0098ba5762..8f7ae46d27 100644 --- a/src/web/Authorized.tsx +++ b/src/web/Authorized.tsx @@ -8,6 +8,7 @@ import {useLocation} from 'react-router'; import useGmp from 'web/hooks/useGmp'; import useUserIsLoggedIn from 'web/hooks/useUserIsLoggedIn'; import useUserName from 'web/hooks/useUserName'; +import {ROUTES} from 'web/route-paths'; import {saveLastVisitedPage} from 'web/utils/user-last-visited-page'; interface AuthorizedProps { @@ -22,7 +23,7 @@ const Authorized = ({children}: AuthorizedProps) => { const username = useUserName(); const logout = useCallback(() => { - if (username && location.pathname !== '/login') { + if (username && location.pathname !== ROUTES.login.url) { const currentPath = `${location.pathname}${location.search ?? ''}`; saveLastVisitedPage(username, currentPath); } diff --git a/src/web/Routes.tsx b/src/web/Routes.tsx index de3c5b1271..1139dd0b3e 100644 --- a/src/web/Routes.tsx +++ b/src/web/Routes.tsx @@ -17,6 +17,7 @@ import SessionTracker from 'web/components/observer/SessionTracker'; import useUserIsLoggedIn from 'web/hooks/useUserIsLoggedIn'; import LoginPageRoute from 'web/pages/login/LoginPageRoute'; import Page from 'web/pages/Page'; +import {ROUTES} from 'web/route-paths'; // Layout components const LoggedOutLayout = () => ; @@ -24,7 +25,7 @@ const LoggedOutLayout = () => ; const LoggedInLayout = () => { const isLoggedIn = useUserIsLoggedIn(); if (!isLoggedIn) { - return ; + return ; } return ( @@ -45,7 +46,7 @@ const loggedInRoutes = [ children: [ // Dashboard { - path: 'dashboards', + path: ROUTES.dashboards.path, lazy: async () => ({ Component: (await import('web/pages/start/StartPage')).default, }), @@ -53,7 +54,7 @@ const loggedInRoutes = [ // Agent routes { - path: 'agent-installers', + path: ROUTES.agentInstallers.path, lazy: async () => ({ Component: ( await import('web/pages/agent-remote-installer/AgentInstallInstructionsPage') @@ -61,13 +62,13 @@ const loggedInRoutes = [ }), }, { - path: 'agents', + path: ROUTES.agents.path, lazy: async () => ({ Component: (await import('web/pages/agents/AgentListPage')).default, }), }, { - path: 'agent-groups', + path: ROUTES.agentGroups.path, lazy: async () => ({ Component: ( await import('web/pages/agent-groups/AgentGroupsListPage') @@ -77,13 +78,13 @@ const loggedInRoutes = [ // Alert routes { - path: 'alerts', + path: ROUTES.alerts.path, lazy: async () => ({ Component: (await import('web/pages/alerts/ListPage')).default, }), }, { - path: 'alert/:id', + path: ROUTES.alert.path, lazy: async () => ({ Component: (await import('web/pages/alerts/DetailsPage')).default, }), @@ -91,33 +92,33 @@ const loggedInRoutes = [ // Audit routes { - path: 'audits', + path: ROUTES.audits.path, lazy: async () => ({ Component: (await import('web/pages/audits/ListPage')).default, }), }, { - path: 'audit/:id', + path: ROUTES.audit.path, lazy: async () => ({ Component: (await import('web/pages/audits/DetailsPage')).default, }), }, { - path: 'auditreports', + path: ROUTES.legacy.auditReports.path, loader: () => { - throw redirect('/audit-reports'); + throw redirect(ROUTES.auditReports.url); }, }, { - path: 'audit-reports', + path: ROUTES.auditReports.path, lazy: async () => ({ Component: (await import('web/pages/reports/AuditReportsListPage')) .default, }), }, { - path: 'audit-report/delta/:id/:deltaid', + path: ROUTES.auditReportDelta.path, lazy: async () => ({ Component: ( await import('web/pages/reports/AuditDeltaReportDetailsPage') @@ -125,13 +126,13 @@ const loggedInRoutes = [ }), }, { - path: 'auditreport/:id', + path: ROUTES.legacy.auditReport.path, loader: ({params}) => { - throw redirect(`/audit-report/${params.id}`); + throw redirect(ROUTES.auditReport.url(params.id ?? '')); }, }, { - path: 'audit-report/:id', + path: ROUTES.auditReport.path, lazy: async () => ({ Component: (await import('web/pages/reports/AuditReportDetailsPage')) .default, @@ -140,25 +141,25 @@ const loggedInRoutes = [ // CERT-Bund routes { - path: 'certbunds', + path: ROUTES.legacy.certBundAdvisories.path, loader: () => { - throw redirect('/cert-bund-advisories'); + throw redirect(ROUTES.certBundAdvisories.url); }, }, { - path: 'cert-bund-advisories', + path: ROUTES.certBundAdvisories.path, lazy: async () => ({ Component: (await import('web/pages/certbund/ListPage')).default, }), }, { - path: 'certbund/:id', + path: ROUTES.legacy.certBundAdvisory.path, loader: ({params}) => { - throw redirect(`/cert-bund-advisory/${params.id}`); + throw redirect(ROUTES.certBundAdvisory.url(params.id ?? '')); }, }, { - path: 'cert-bund-advisory/:id', + path: ROUTES.certBundAdvisory.path, lazy: async () => ({ Component: (await import('web/pages/certbund/DetailsPage')).default, }), @@ -166,13 +167,13 @@ const loggedInRoutes = [ // Container Image Target routes { - path: 'ociimagetargets', + path: ROUTES.legacy.ociImageTargets.path, loader: () => { - throw redirect('/oci-image-targets'); + throw redirect(ROUTES.ociImageTargets.url); }, }, { - path: 'oci-image-targets', + path: ROUTES.ociImageTargets.path, lazy: async () => ({ Component: ( await import('web/pages/container-image-targets/ContainerImageTargetsListPage') @@ -181,13 +182,13 @@ const loggedInRoutes = [ }, { - path: 'webapplicationtargets', + path: ROUTES.legacy.webApplicationTargets.path, loader: () => { - throw redirect('/web-application-targets'); + throw redirect(ROUTES.webApplicationTargets.url); }, }, { - path: 'web-application-targets', + path: ROUTES.webApplicationTargets.path, lazy: async () => ({ Component: ( await import('web/pages/web-application-targets/WebApplicationTargetsListPage') @@ -197,13 +198,13 @@ const loggedInRoutes = [ // CPE routes { - path: 'cpes', + path: ROUTES.cpes.path, lazy: async () => ({ Component: (await import('web/pages/cpes/ListPage')).default, }), }, { - path: 'cpe/:id', + path: ROUTES.cpe.path, lazy: async () => ({ Component: (await import('web/pages/cpes/DetailsPage')).default, }), @@ -211,14 +212,14 @@ const loggedInRoutes = [ // Credential routes { - path: 'credentials', + path: ROUTES.credentials.path, lazy: async () => ({ Component: (await import('web/pages/credentials/CredentialListPage')) .default, }), }, { - path: 'credential/:id', + path: ROUTES.credential.path, lazy: async () => ({ Component: ( await import('web/pages/credentials/CredentialDetailsPage') @@ -227,13 +228,13 @@ const loggedInRoutes = [ }, { - path: 'credentialstore', + path: ROUTES.legacy.credentialStore.path, loader: () => { - throw redirect('/credential-store'); + throw redirect(ROUTES.credentialStore.url); }, }, { - path: 'credential-store', + path: ROUTES.credentialStore.path, lazy: async () => ({ Component: ( await import('web/pages/credential-store/CredentialStorePage') @@ -243,13 +244,13 @@ const loggedInRoutes = [ // CVE routes { - path: 'cves', + path: ROUTES.cves.path, lazy: async () => ({ Component: (await import('web/pages/cves/ListPage')).default, }), }, { - path: 'cve/:id', + path: ROUTES.cve.path, lazy: async () => ({ Component: (await import('web/pages/cves/DetailsPage')).default, }), @@ -257,25 +258,25 @@ const loggedInRoutes = [ // DFN-CERT routes { - path: 'dfncerts', + path: ROUTES.legacy.dfnCertAdvisories.path, loader: () => { - throw redirect('/dfn-cert-advisories'); + throw redirect(ROUTES.dfnCertAdvisories.url); }, }, { - path: 'dfn-cert-advisories', + path: ROUTES.dfnCertAdvisories.path, lazy: async () => ({ Component: (await import('web/pages/dfncert/ListPage')).default, }), }, { - path: 'dfncert/:id', + path: ROUTES.legacy.dfnCertAdvisory.path, loader: ({params}) => { - throw redirect(`/dfn-cert-advisory/${params.id}`); + throw redirect(ROUTES.dfnCertAdvisory.url(params.id ?? '')); }, }, { - path: 'dfn-cert-advisory/:id', + path: ROUTES.dfnCertAdvisory.path, lazy: async () => ({ Component: (await import('web/pages/dfncert/DetailsPage')).default, }), @@ -283,13 +284,13 @@ const loggedInRoutes = [ // Feed Status route { - path: 'feedstatus', + path: ROUTES.legacy.feedStatus.path, loader: () => { - throw redirect('/feed-status'); + throw redirect(ROUTES.feedStatus.url); }, }, { - path: 'feed-status', + path: ROUTES.feedStatus.path, lazy: async () => ({ Component: (await import('web/pages/extras/FeedStatusPage')).default, }), @@ -297,13 +298,13 @@ const loggedInRoutes = [ // Filter routes { - path: 'filters', + path: ROUTES.filters.path, lazy: async () => ({ Component: (await import('web/pages/filters/ListPage')).default, }), }, { - path: 'filter/:id', + path: ROUTES.filter.path, lazy: async () => ({ Component: (await import('web/pages/filters/DetailsPage')).default, }), @@ -311,13 +312,13 @@ const loggedInRoutes = [ // Group routes { - path: 'groups', + path: ROUTES.groups.path, lazy: async () => ({ Component: (await import('web/pages/groups/ListPage')).default, }), }, { - path: 'group/:id', + path: ROUTES.group.path, lazy: async () => ({ Component: (await import('web/pages/groups/DetailsPage')).default, }), @@ -325,13 +326,13 @@ const loggedInRoutes = [ // Host routes { - path: 'hosts', + path: ROUTES.hosts.path, lazy: async () => ({ Component: (await import('web/pages/hosts/ListPage')).default, }), }, { - path: 'host/:id', + path: ROUTES.host.path, lazy: async () => ({ Component: (await import('web/pages/hosts/DetailsPage')).default, }), @@ -339,7 +340,7 @@ const loggedInRoutes = [ // LDAP route { - path: 'ldap', + path: ROUTES.ldap.path, lazy: async () => ({ Component: (await import('web/pages/ldap/LdapPage')).default, }), @@ -347,13 +348,13 @@ const loggedInRoutes = [ // Note routes { - path: 'notes', + path: ROUTES.notes.path, lazy: async () => ({ Component: (await import('web/pages/notes/NoteListPage')).default, }), }, { - path: 'note/:id', + path: ROUTES.note.path, lazy: async () => ({ Component: (await import('web/pages/notes/NoteDetailsPage')).default, }), @@ -361,13 +362,13 @@ const loggedInRoutes = [ // NVT routes { - path: 'nvts', + path: ROUTES.nvts.path, lazy: async () => ({ Component: (await import('web/pages/nvts/ListPage')).default, }), }, { - path: 'nvt/:id', + path: ROUTES.nvt.path, lazy: async () => ({ Component: (await import('web/pages/nvts/DetailsPage')).default, }), @@ -375,26 +376,26 @@ const loggedInRoutes = [ // Operating System routes { - path: 'operatingsystems', + path: ROUTES.legacy.operatingSystems.path, loader: () => { - throw redirect('/operating-systems'); + throw redirect(ROUTES.operatingSystems.url); }, }, { - path: 'operating-systems', + path: ROUTES.operatingSystems.path, lazy: async () => ({ Component: (await import('web/pages/operatingsystems/ListPage')) .default, }), }, { - path: 'operatingsystem/:id', + path: ROUTES.legacy.operatingSystem.path, loader: ({params}) => { - throw redirect(`/operating-system/${params.id}`); + throw redirect(ROUTES.operatingSystem.url(params.id ?? '')); }, }, { - path: 'operating-system/:id', + path: ROUTES.operatingSystem.path, lazy: async () => ({ Component: (await import('web/pages/operatingsystems/DetailsPage')) .default, @@ -403,14 +404,14 @@ const loggedInRoutes = [ // Override routes { - path: 'overrides', + path: ROUTES.overrides.path, lazy: async () => ({ Component: (await import('web/pages/overrides/OverrideListPage')) .default, }), }, { - path: 'override/:id', + path: ROUTES.override.path, lazy: async () => ({ Component: (await import('web/pages/overrides/OverrideDetailsPage')) .default, @@ -419,7 +420,7 @@ const loggedInRoutes = [ // Performance route { - path: 'performance', + path: ROUTES.performance.path, lazy: async () => ({ Component: (await import('web/pages/performance/PerformancePage')) .default, @@ -428,14 +429,14 @@ const loggedInRoutes = [ // Permission routes { - path: 'permissions', + path: ROUTES.permissions.path, lazy: async () => ({ Component: (await import('web/pages/permissions/PermissionListPage')) .default, }), }, { - path: 'permission/:id', + path: ROUTES.permission.path, lazy: async () => ({ Component: ( await import('web/pages/permissions/PermissionDetailsPage') @@ -445,13 +446,13 @@ const loggedInRoutes = [ // Policy routes { - path: 'policies', + path: ROUTES.policies.path, lazy: async () => ({ Component: (await import('web/pages/policies/ListPage')).default, }), }, { - path: 'policy/:id', + path: ROUTES.policy.path, lazy: async () => ({ Component: (await import('web/pages/policies/DetailsPage')).default, }), @@ -459,26 +460,26 @@ const loggedInRoutes = [ // Port List routes { - path: 'portlists', + path: ROUTES.legacy.portLists.path, loader: () => { - throw redirect('/port-lists'); + throw redirect(ROUTES.portLists.url); }, }, { - path: 'port-lists', + path: ROUTES.portLists.path, lazy: async () => ({ Component: (await import('web/pages/portlists/PortListListPage')) .default, }), }, { - path: 'portlist/:id', + path: ROUTES.legacy.portList.path, loader: ({params}) => { - throw redirect(`/port-list/${params.id}`); + throw redirect(ROUTES.portList.url(params.id ?? '')); }, }, { - path: 'port-list/:id', + path: ROUTES.portList.path, lazy: async () => ({ Component: (await import('web/pages/portlists/PortListDetailsPage')) .default, @@ -487,7 +488,7 @@ const loggedInRoutes = [ // RADIUS route { - path: 'radius', + path: ROUTES.radius.path, lazy: async () => ({ Component: (await import('web/pages/radius/RadiusPage')).default, }), @@ -495,20 +496,20 @@ const loggedInRoutes = [ // Report routes { - path: 'reports', + path: ROUTES.reports.path, lazy: async () => ({ Component: (await import('web/pages/reports/ReportListPage')).default, }), }, { - path: 'report/delta/:id/:deltaid', + path: ROUTES.reportDelta.path, lazy: async () => ({ Component: (await import('web/pages/reports/DeltaDetailsPage')) .default, }), }, { - path: 'report/:id', + path: ROUTES.report.path, lazy: async () => ({ Component: (await import('web/pages/reports/ReportDetailsPage')) .default, @@ -517,25 +518,25 @@ const loggedInRoutes = [ // Report Config routes { - path: 'reportconfigs', + path: ROUTES.legacy.reportConfigs.path, loader: () => { - throw redirect('/report-configs'); + throw redirect(ROUTES.reportConfigs.url); }, }, { - path: 'report-configs', + path: ROUTES.reportConfigs.path, lazy: async () => ({ Component: (await import('web/pages/reportconfigs/ListPage')).default, }), }, { - path: 'reportconfig/:id', + path: ROUTES.legacy.reportConfig.path, loader: ({params}) => { - throw redirect(`/report-config/${params.id}`); + throw redirect(ROUTES.reportConfig.url(params.id ?? '')); }, }, { - path: 'report-config/:id', + path: ROUTES.reportConfig.path, lazy: async () => ({ Component: (await import('web/pages/reportconfigs/DetailsPage')) .default, @@ -544,25 +545,25 @@ const loggedInRoutes = [ // Report Format routes { - path: 'reportformats', + path: ROUTES.legacy.reportFormats.path, loader: () => { - throw redirect('/report-formats'); + throw redirect(ROUTES.reportFormats.url); }, }, { - path: 'report-formats', + path: ROUTES.reportFormats.path, lazy: async () => ({ Component: (await import('web/pages/reportformats/ListPage')).default, }), }, { - path: 'reportformat/:id', + path: ROUTES.legacy.reportFormat.path, loader: ({params}) => { - throw redirect(`/report-format/${params.id}`); + throw redirect(ROUTES.reportFormat.url(params.id ?? '')); }, }, { - path: 'report-format/:id', + path: ROUTES.reportFormat.path, lazy: async () => ({ Component: (await import('web/pages/reportformats/DetailsPage')) .default, @@ -571,13 +572,13 @@ const loggedInRoutes = [ // Result routes { - path: 'results', + path: ROUTES.results.path, lazy: async () => ({ Component: (await import('web/pages/results/ListPage')).default, }), }, { - path: 'result/:id', + path: ROUTES.result.path, lazy: async () => ({ Component: (await import('web/pages/results/DetailsPage')).default, }), @@ -585,13 +586,13 @@ const loggedInRoutes = [ // Role routes { - path: 'roles', + path: ROUTES.roles.path, lazy: async () => ({ Component: (await import('web/pages/roles/RoleListPage')).default, }), }, { - path: 'role/:id', + path: ROUTES.role.path, lazy: async () => ({ Component: (await import('web/pages/roles/RoleDetailsPage')).default, }), @@ -599,25 +600,25 @@ const loggedInRoutes = [ // Scan Config routes { - path: 'scanconfigs', + path: ROUTES.legacy.scanConfigs.path, loader: () => { - throw redirect('/scan-configs'); + throw redirect(ROUTES.scanConfigs.url); }, }, { - path: 'scan-configs', + path: ROUTES.scanConfigs.path, lazy: async () => ({ Component: (await import('web/pages/scanconfigs/ListPage')).default, }), }, { - path: 'scanconfig/:id', + path: ROUTES.legacy.scanConfig.path, loader: ({params}) => { - throw redirect(`/scan-config/${params.id}`); + throw redirect(ROUTES.scanConfig.url(params.id ?? '')); }, }, { - path: 'scan-config/:id', + path: ROUTES.scanConfig.path, lazy: async () => ({ Component: (await import('web/pages/scanconfigs/DetailsPage')) .default, @@ -626,14 +627,14 @@ const loggedInRoutes = [ // Scanner routes { - path: 'scanners', + path: ROUTES.scanners.path, lazy: async () => ({ Component: (await import('web/pages/scanners/ScannerListPage')) .default, }), }, { - path: 'scanner/:id', + path: ROUTES.scanner.path, lazy: async () => ({ Component: (await import('web/pages/scanners/ScannerDetailsPage')) .default, @@ -642,13 +643,13 @@ const loggedInRoutes = [ // Schedule routes { - path: 'schedules', + path: ROUTES.schedules.path, lazy: async () => ({ Component: (await import('web/pages/schedules/ListPage')).default, }), }, { - path: 'schedule/:id', + path: ROUTES.schedule.path, lazy: async () => ({ Component: (await import('web/pages/schedules/DetailsPage')).default, }), @@ -656,13 +657,13 @@ const loggedInRoutes = [ // Tag routes { - path: 'tags', + path: ROUTES.tags.path, lazy: async () => ({ Component: (await import('web/pages/tags/TagListPage')).default, }), }, { - path: 'tag/:id', + path: ROUTES.tag.path, lazy: async () => ({ Component: (await import('web/pages/tags/TagDetailsPage')).default, }), @@ -670,13 +671,13 @@ const loggedInRoutes = [ // Target routes { - path: 'targets', + path: ROUTES.targets.path, lazy: async () => ({ Component: (await import('web/pages/targets/TargetListPage')).default, }), }, { - path: 'target/:id', + path: ROUTES.target.path, lazy: async () => ({ Component: (await import('web/pages/targets/TargetDetailsPage')) .default, @@ -685,13 +686,13 @@ const loggedInRoutes = [ // Task routes { - path: 'tasks', + path: ROUTES.tasks.path, lazy: async () => ({ Component: (await import('web/pages/tasks/TaskListPage')).default, }), }, { - path: 'task/:id', + path: ROUTES.task.path, lazy: async () => ({ Component: (await import('web/pages/tasks/TaskDetailsPage')).default, }), @@ -699,14 +700,14 @@ const loggedInRoutes = [ // Ticket routes { - path: 'tickets', + path: ROUTES.tickets.path, lazy: async () => ({ Component: (await import('web/pages/tickets/TicketsListPage')) .default, }), }, { - path: 'ticket/:id', + path: ROUTES.ticket.path, lazy: async () => ({ Component: (await import('web/pages/tickets/TicketDetailsPage')) .default, @@ -715,26 +716,26 @@ const loggedInRoutes = [ // TLS Certificate routes { - path: 'tlscertificates', + path: ROUTES.legacy.tlsCertificates.path, loader: () => { - throw redirect('/tls-certificates'); + throw redirect(ROUTES.tlsCertificates.url); }, }, { - path: 'tls-certificates', + path: ROUTES.tlsCertificates.path, lazy: async () => ({ Component: (await import('web/pages/tlscertificates/ListPage')) .default, }), }, { - path: 'tlscertificate/:id', + path: ROUTES.legacy.tlsCertificate.path, loader: ({params}) => { - throw redirect(`/tls-certificate/${params.id}`); + throw redirect(ROUTES.tlsCertificate.url(params.id ?? '')); }, }, { - path: 'tls-certificate/:id', + path: ROUTES.tlsCertificate.path, lazy: async () => ({ Component: (await import('web/pages/tlscertificates/DetailsPage')) .default, @@ -743,7 +744,7 @@ const loggedInRoutes = [ // Trashcan route { - path: 'trashcan', + path: ROUTES.trashcan.path, lazy: async () => ({ Component: (await import('web/pages/trashcan/TrashCanPage')).default, }), @@ -751,13 +752,13 @@ const loggedInRoutes = [ // User routes { - path: 'users', + path: ROUTES.users.path, lazy: async () => ({ Component: (await import('web/pages/users/UsersListPage')).default, }), }, { - path: 'user/:id', + path: ROUTES.user.path, lazy: async () => ({ Component: (await import('web/pages/users/UserDetailsPage')).default, }), @@ -765,13 +766,13 @@ const loggedInRoutes = [ // User Settings route { - path: 'usersettings', + path: ROUTES.legacy.userSettings.path, loader: () => { - throw redirect('/user-settings'); + throw redirect(ROUTES.userSettings.url); }, }, { - path: 'user-settings', + path: ROUTES.userSettings.path, lazy: async () => ({ Component: (await import('web/pages/user-settings/UserSettingsPage')) .default, @@ -780,7 +781,7 @@ const loggedInRoutes = [ // Vulnerability routes { - path: 'vulnerabilities', + path: ROUTES.vulnerabilities.path, lazy: async () => ({ Component: ( await import('web/pages/vulnerabilities/VulnerabilitiesListPage') @@ -790,13 +791,13 @@ const loggedInRoutes = [ // CVSS Calculator route { - path: 'cvsscalculator', + path: ROUTES.legacy.cvssCalculator.path, loader: () => { - throw redirect('/cvss-calculator'); + throw redirect(ROUTES.cvssCalculator.url); }, }, { - path: 'cvss-calculator', + path: ROUTES.cvssCalculator.path, lazy: async () => ({ Component: (await import('web/pages/extras/CvssCalculatorPage')) .default, @@ -805,13 +806,13 @@ const loggedInRoutes = [ // Special routes { - path: 'notfound', + path: ROUTES.legacy.notFound.path, loader: () => { - throw redirect('/not-found'); + throw redirect(ROUTES.notFound.url); }, }, { - path: 'not-found', + path: ROUTES.notFound.path, lazy: async () => ({ Component: (await import('web/pages/NotFoundPage')).default, }), @@ -820,7 +821,7 @@ const loggedInRoutes = [ // Root redirect for logged-in users { index: true, - element: , + element: , }, // Catch all for logged-in users @@ -837,9 +838,9 @@ const loggedInRoutes = [ const AuthRedirect = () => { const isLoggedIn = useUserIsLoggedIn(); return isLoggedIn ? ( - + ) : ( - + ); }; @@ -854,11 +855,11 @@ const router = createBrowserRouter([ element: , }, { - path: 'login', + path: ROUTES.login.path, element: , }, { - path: 'omp', + path: ROUTES.omp.path, lazy: async () => ({ Component: (await import('web/pages/OmpPage')).default, }), diff --git a/src/web/__tests__/route-paths.test.ts b/src/web/__tests__/route-paths.test.ts new file mode 100644 index 0000000000..45e978f5b6 --- /dev/null +++ b/src/web/__tests__/route-paths.test.ts @@ -0,0 +1,79 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, expect, test} from 'vitest'; +import {entityListURL, entityURL, ROUTES, routeMatch} from 'web/route-paths'; + +describe('route paths', () => { + test('keeps canonical and legacy audit paths distinct', () => { + expect(ROUTES.auditReports.path).toBe('audit-reports'); + expect(ROUTES.auditReports.url).toBe('/audit-reports'); + expect(ROUTES.legacy.auditReports.path).toBe('auditreports'); + expect(ROUTES.legacy.auditReports.url).toBe('/auditreports'); + }); + + test('provides relative router patterns for parameterized routes', () => { + expect(ROUTES.auditReport.path).toBe('audit-report/:id'); + expect(ROUTES.auditReportDelta.path).toBe( + 'audit-report/delta/:id/:deltaid', + ); + expect(ROUTES.legacy.auditReport.path).toBe('auditreport/:id'); + }); + + test('encodes parameterized URLs', () => { + expect(ROUTES.auditReport.url('report/id')).toBe( + '/audit-report/report%2Fid', + ); + expect(ROUTES.auditReportDelta.url('report/id', 'delta#1')).toBe( + '/audit-report/delta/report%2Fid/delta%231', + ); + }); + + test('builds entity URLs from canonical route descriptors', () => { + expect(entityURL('auditreport', 'report/id')).toBe( + '/audit-report/report%2Fid', + ); + expect(entityURL('operatingsystem', 'os-id')).toBe( + '/operating-system/os-id', + ); + expect(entityListURL('auditreport')).toBe('/audit-reports'); + expect(entityListURL('operatingsystem')).toBe('/operating-systems'); + }); + + test('keeps every compatibility alias explicit', () => { + expect(Object.keys(ROUTES.legacy)).toEqual([ + 'auditReports', + 'auditReport', + 'certBundAdvisories', + 'certBundAdvisory', + 'ociImageTargets', + 'webApplicationTargets', + 'credentialStore', + 'dfnCertAdvisories', + 'dfnCertAdvisory', + 'feedStatus', + 'operatingSystems', + 'operatingSystem', + 'portLists', + 'portList', + 'reportConfigs', + 'reportConfig', + 'reportFormats', + 'reportFormat', + 'scanConfigs', + 'scanConfig', + 'tlsCertificates', + 'tlsCertificate', + 'userSettings', + 'cvssCalculator', + 'notFound', + ]); + }); + + test('derives menu wildcard patterns from route patterns', () => { + expect(routeMatch(ROUTES.report.path)).toBe('/report/*'); + expect(routeMatch(ROUTES.reports.path)).toBe('/reports'); + }); +}); diff --git a/src/web/components/link/DetailsLink.tsx b/src/web/components/link/DetailsLink.tsx index f905a5da1d..65078ca97c 100644 --- a/src/web/components/link/DetailsLink.tsx +++ b/src/web/components/link/DetailsLink.tsx @@ -3,10 +3,11 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import {entityURL, type EntityType} from 'gmp/utils/entity-type'; +import {type EntityType} from 'gmp/utils/entity-type'; import {isDefined} from 'gmp/utils/identity'; import Link, {type LinkProps} from 'web/components/link/Link'; import useCapabilities from 'web/hooks/useCapabilities'; +import {entityURL} from 'web/route-paths'; export interface DetailsLinkProps extends LinkProps { id: string; diff --git a/src/web/components/menu/Menu.tsx b/src/web/components/menu/Menu.tsx index 13fca0c958..81228526d8 100644 --- a/src/web/components/menu/Menu.tsx +++ b/src/web/components/menu/Menu.tsx @@ -22,6 +22,7 @@ import useCapabilities from 'web/hooks/useCapabilities'; import useFeatures from 'web/hooks/useFeatures'; import useGmp from 'web/hooks/useGmp'; import useTranslation from 'web/hooks/useTranslation'; +import {routeMatch, ROUTES} from 'web/route-paths'; const Menu = () => { const [_] = useTranslation(); @@ -29,185 +30,211 @@ const Menu = () => { const features = useFeatures(); const gmp = useGmp(); - const dashboardsMatch = useMatch('/dashboards'); + const dashboardsMatch = useMatch(routeMatch(ROUTES.dashboards.path)); const isDashboardsActive = Boolean(dashboardsMatch); - const tasksMatch = useMatch('/tasks'); - const taskMatch = useMatch('/task/*'); + const tasksMatch = useMatch(routeMatch(ROUTES.tasks.path)); + const taskMatch = useMatch(routeMatch(ROUTES.task.path)); const isTasksActive = Boolean(tasksMatch || taskMatch); const location = useLocation(); - const reportsMatch = useMatch('/reports'); - const reportMatch = useMatch('/report/*'); + const reportsMatch = useMatch(routeMatch(ROUTES.reports.path)); + const reportMatch = useMatch(routeMatch(ROUTES.report.path)); const isReportsActive = Boolean(reportsMatch || reportMatch); - const resultsMatch = useMatch('/results'); - const resultMatch = useMatch('/result/*'); + const resultsMatch = useMatch(routeMatch(ROUTES.results.path)); + const resultMatch = useMatch(routeMatch(ROUTES.result.path)); const isResultsActive = Boolean(resultsMatch || resultMatch); - const vulnerabilitiesMatch = useMatch('/vulnerabilities'); - const vulnerabilityMatch = useMatch('/vulnerability/*'); + const vulnerabilitiesMatch = useMatch( + routeMatch(ROUTES.vulnerabilities.path), + ); + const vulnerabilityMatch = useMatch(routeMatch(ROUTES.vulnerability.path)); const isVulnerabilitiesActive = Boolean( vulnerabilitiesMatch || vulnerabilityMatch, ); - const notesMatch = useMatch('/notes'); - const noteMatch = useMatch('/note/*'); + const notesMatch = useMatch(routeMatch(ROUTES.notes.path)); + const noteMatch = useMatch(routeMatch(ROUTES.note.path)); const isNotesActive = Boolean(notesMatch || noteMatch); - const overridesMatch = useMatch('/overrides'); - const overrideMatch = useMatch('/override/*'); + const overridesMatch = useMatch(routeMatch(ROUTES.overrides.path)); + const overrideMatch = useMatch(routeMatch(ROUTES.override.path)); const isOverridesActive = Boolean(overridesMatch || overrideMatch); - const hostsMatch = useMatch('/hosts'); - const hostDetailsMatch = useMatch('/host/*'); + const hostsMatch = useMatch(routeMatch(ROUTES.hosts.path)); + const hostDetailsMatch = useMatch(routeMatch(ROUTES.host.path)); const isHostsActive = Boolean(hostsMatch || hostDetailsMatch); - const operatingSystemsMatch = useMatch('/operating-systems'); - const operatingSystemMatch = useMatch('/operating-system/*'); + const operatingSystemsMatch = useMatch( + routeMatch(ROUTES.operatingSystems.path), + ); + const operatingSystemMatch = useMatch( + routeMatch(ROUTES.operatingSystem.path), + ); const isOperatingSystemsActive = Boolean( operatingSystemsMatch || operatingSystemMatch, ); - const tlsCertificatesMatch = useMatch('/tls-certificates'); - const tlsCertificateMatch = useMatch('/tls-certificate/*'); + const tlsCertificatesMatch = useMatch( + routeMatch(ROUTES.tlsCertificates.path), + ); + const tlsCertificateMatch = useMatch(routeMatch(ROUTES.tlsCertificate.path)); const isTlsCertificatesActive = Boolean( tlsCertificatesMatch || tlsCertificateMatch, ); - const ticketsMatch = useMatch('/tickets'); - const ticketMatch = useMatch('/ticket/*'); + const ticketsMatch = useMatch(routeMatch(ROUTES.tickets.path)); + const ticketMatch = useMatch(routeMatch(ROUTES.ticket.path)); const isTicketsActive = Boolean(ticketsMatch || ticketMatch); - const policiesMatch = useMatch('/policies'); - const policyMatch = useMatch('/policy/*'); + const policiesMatch = useMatch(routeMatch(ROUTES.policies.path)); + const policyMatch = useMatch(routeMatch(ROUTES.policy.path)); const isPoliciesActive = Boolean(policiesMatch || policyMatch); - const auditsMatch = useMatch('/audits'); - const auditMatch = useMatch('/audit/*'); + const auditsMatch = useMatch(routeMatch(ROUTES.audits.path)); + const auditMatch = useMatch(routeMatch(ROUTES.audit.path)); const isAuditsActive = Boolean(auditsMatch || auditMatch); - const auditReportsMatch = useMatch('/audit-reports'); - const auditReportMatch = useMatch('/audit-report/*'); + const auditReportsMatch = useMatch(routeMatch(ROUTES.auditReports.path)); + const auditReportMatch = useMatch(routeMatch(ROUTES.auditReport.path)); const isAuditReportsActive = Boolean(auditReportsMatch || auditReportMatch); - const nvtsMatch = useMatch('/nvts'); - const nvtMatch = useMatch('/nvt/*'); + const nvtsMatch = useMatch(routeMatch(ROUTES.nvts.path)); + const nvtMatch = useMatch(routeMatch(ROUTES.nvt.path)); const isNvtsActive = Boolean(nvtsMatch || nvtMatch); - const cvesMatch = useMatch('/cves'); - const cveMatch = useMatch('/cve/*'); + const cvesMatch = useMatch(routeMatch(ROUTES.cves.path)); + const cveMatch = useMatch(routeMatch(ROUTES.cve.path)); const isCvesActive = Boolean(cvesMatch || cveMatch); - const cpesMatch = useMatch('/cpes'); - const cpeMatch = useMatch('/cpe/*'); + const cpesMatch = useMatch(routeMatch(ROUTES.cpes.path)); + const cpeMatch = useMatch(routeMatch(ROUTES.cpe.path)); const isCpesActive = Boolean(cpesMatch || cpeMatch); - const certbundsMatch = useMatch('/cert-bund-advisories'); - const certbundMatch = useMatch('/cert-bund-advisory/*'); + const certbundsMatch = useMatch(routeMatch(ROUTES.certBundAdvisories.path)); + const certbundMatch = useMatch(routeMatch(ROUTES.certBundAdvisory.path)); const isCertbundsActive = Boolean(certbundsMatch || certbundMatch); - const dfncertsMatch = useMatch('/dfn-cert-advisories'); - const dfncertMatch = useMatch('/dfn-cert-advisory/*'); + const dfncertsMatch = useMatch(routeMatch(ROUTES.dfnCertAdvisories.path)); + const dfncertMatch = useMatch(routeMatch(ROUTES.dfnCertAdvisory.path)); const isDfncertsActive = Boolean(dfncertsMatch || dfncertMatch); - const targetsMatch = useMatch('/targets'); - const targetMatch = useMatch('/target/*'); + const targetsMatch = useMatch(routeMatch(ROUTES.targets.path)); + const targetMatch = useMatch(routeMatch(ROUTES.target.path)); const isTargetsActive = Boolean(targetsMatch || targetMatch); - const portlistsMatch = useMatch('/port-lists'); - const portlistMatch = useMatch('/port-list/*'); + const portlistsMatch = useMatch(routeMatch(ROUTES.portLists.path)); + const portlistMatch = useMatch(routeMatch(ROUTES.portList.path)); const isPortlistsActive = Boolean(portlistsMatch || portlistMatch); - const credentialsMatch = useMatch('/credentials'); - const credentialMatch = useMatch('/credential/*'); + const credentialsMatch = useMatch(routeMatch(ROUTES.credentials.path)); + const credentialMatch = useMatch(routeMatch(ROUTES.credential.path)); const isCredentialsActive = Boolean(credentialsMatch || credentialMatch); - const scanConfigsMatch = useMatch('/scan-configs'); - const scanConfigMatch = useMatch('/scan-config/*'); + const scanConfigsMatch = useMatch(routeMatch(ROUTES.scanConfigs.path)); + const scanConfigMatch = useMatch(routeMatch(ROUTES.scanConfig.path)); const isScanConfigsActive = Boolean(scanConfigsMatch || scanConfigMatch); - const alertsMatch = useMatch('/alerts'); - const alertMatch = useMatch('/alert/*'); + const alertsMatch = useMatch(routeMatch(ROUTES.alerts.path)); + const alertMatch = useMatch(routeMatch(ROUTES.alert.path)); const isAlertsActive = Boolean(alertsMatch || alertMatch); - const agentsMatch = useMatch('/agents'); - const agentMatch = useMatch('/agent/*'); + const agentsMatch = useMatch(routeMatch(ROUTES.agents.path)); + const agentMatch = useMatch(routeMatch(ROUTES.agent.path)); const isAgentsActive = Boolean(agentsMatch || agentMatch); - const agentGroupsMatch = useMatch('/agent-groups'); - const agentGroupMatch = useMatch('/agent-group/*'); + const agentGroupsMatch = useMatch(routeMatch(ROUTES.agentGroups.path)); + const agentGroupMatch = useMatch(routeMatch(ROUTES.agentGroup.path)); const isAgentGroupsActive = Boolean(agentGroupsMatch || agentGroupMatch); - const agentInstallersMatch = useMatch('/agent-installers'); - const agentInstallerMatch = useMatch('/agent-installer/*'); + const agentInstallersMatch = useMatch( + routeMatch(ROUTES.agentInstallers.path), + ); + const agentInstallerMatch = useMatch(routeMatch(ROUTES.agentInstaller.path)); const isAgentInstallersActive = Boolean( agentInstallersMatch || agentInstallerMatch, ); - const containerImageTargetsMatch = useMatch('/oci-image-targets'); - const containerImageTargetMatch = useMatch('/oci-image-target/*'); + const containerImageTargetsMatch = useMatch( + routeMatch(ROUTES.ociImageTargets.path), + ); + const containerImageTargetMatch = useMatch( + routeMatch(ROUTES.containerImageTarget.path), + ); const isContainerImageTargetsActive = Boolean( containerImageTargetsMatch || containerImageTargetMatch, ); - const webApplicationTargetsMatch = useMatch('/web-application-targets'); - const webApplicationTargetMatch = useMatch('/web-application-target/*'); + const webApplicationTargetsMatch = useMatch( + routeMatch(ROUTES.webApplicationTargets.path), + ); + const webApplicationTargetMatch = useMatch( + routeMatch(ROUTES.webApplicationTarget.path), + ); const isWebApplicationTargetsActive = Boolean( webApplicationTargetsMatch || webApplicationTargetMatch, ); - const schedulesMatch = useMatch('/schedules'); - const scheduleMatch = useMatch('/schedule/*'); + const schedulesMatch = useMatch(routeMatch(ROUTES.schedules.path)); + const scheduleMatch = useMatch(routeMatch(ROUTES.schedule.path)); const isSchedulesActive = Boolean(schedulesMatch || scheduleMatch); - const reportConfigsMatch = useMatch('/report-configs'); - const reportConfigMatch = useMatch('/report-config/*'); + const reportConfigsMatch = useMatch(routeMatch(ROUTES.reportConfigs.path)); + const reportConfigMatch = useMatch(routeMatch(ROUTES.reportConfig.path)); const isReportConfigsActive = Boolean( reportConfigsMatch || reportConfigMatch, ); - const reportFormatsMatch = useMatch('/report-formats'); - const reportFormatMatch = useMatch('/report-format/*'); + const reportFormatsMatch = useMatch(routeMatch(ROUTES.reportFormats.path)); + const reportFormatMatch = useMatch(routeMatch(ROUTES.reportFormat.path)); const isReportFormatsActive = Boolean( reportFormatsMatch || reportFormatMatch, ); - const scannersMatch = useMatch('/scanners'); - const scannerMatch = useMatch('/scanner/*'); + const scannersMatch = useMatch(routeMatch(ROUTES.scanners.path)); + const scannerMatch = useMatch(routeMatch(ROUTES.scanner.path)); const isScannersActive = Boolean(scannersMatch || scannerMatch); - const filtersMatch = useMatch('/filters'); - const filterMatch = useMatch('/filter/*'); + const filtersMatch = useMatch(routeMatch(ROUTES.filters.path)); + const filterMatch = useMatch(routeMatch(ROUTES.filter.path)); const isFiltersActive = Boolean(filtersMatch || filterMatch); - const tagsMatch = useMatch('/tags'); - const tagMatch = useMatch('/tag/*'); + const tagsMatch = useMatch(routeMatch(ROUTES.tags.path)); + const tagMatch = useMatch(routeMatch(ROUTES.tag.path)); const isTagsActive = Boolean(tagsMatch || tagMatch); - const usersMatch = useMatch('/users'); - const userMatch = useMatch('/user/*'); + const usersMatch = useMatch(routeMatch(ROUTES.users.path)); + const userMatch = useMatch(routeMatch(ROUTES.user.path)); const isUserActive = Boolean(usersMatch || userMatch); - const groupsMatch = useMatch('/groups'); - const groupMatch = useMatch('/group/*'); + const groupsMatch = useMatch(routeMatch(ROUTES.groups.path)); + const groupMatch = useMatch(routeMatch(ROUTES.group.path)); const isGroupsActive = Boolean(groupsMatch || groupMatch); - const rolesMatch = useMatch('/roles'); - const roleMatch = useMatch('/role/*'); + const rolesMatch = useMatch(routeMatch(ROUTES.roles.path)); + const roleMatch = useMatch(routeMatch(ROUTES.role.path)); const isRolesActive = Boolean(rolesMatch || roleMatch); - const permissionsMatch = useMatch('/permissions'); - const permissionMatch = useMatch('/permission/*'); + const permissionsMatch = useMatch(routeMatch(ROUTES.permissions.path)); + const permissionMatch = useMatch(routeMatch(ROUTES.permission.path)); const isPermissionsActive = Boolean(permissionsMatch || permissionMatch); - const isPerformanceActive = Boolean(useMatch('/performance')); - const isTrashcanActive = Boolean(useMatch('/trashcan')); - const isFeedStatusActive = Boolean(useMatch('/feed-status')); - const isLdapActive = Boolean(useMatch('/ldap')); - const isCredentialStoreActive = Boolean(useMatch('/credential-store')); - const isRadiusActive = Boolean(useMatch('/radius')); - const isCvssCalculatorActive = Boolean(useMatch('/cvss-calculator')); + const isPerformanceActive = Boolean( + useMatch(routeMatch(ROUTES.performance.path)), + ); + const isTrashcanActive = Boolean(useMatch(routeMatch(ROUTES.trashcan.path))); + const isFeedStatusActive = Boolean( + useMatch(routeMatch(ROUTES.feedStatus.path)), + ); + const isLdapActive = Boolean(useMatch(routeMatch(ROUTES.ldap.path))); + const isCredentialStoreActive = Boolean( + useMatch(routeMatch(ROUTES.credentialStore.path)), + ); + const isRadiusActive = Boolean(useMatch(routeMatch(ROUTES.radius.path))); + const isCvssCalculatorActive = Boolean( + useMatch(routeMatch(ROUTES.cvssCalculator.path)), + ); const mayAccessAny = (keys: EntityType[]) => keys.some(key => isDefined(capabilities) && capabilities.mayAccess(key)); @@ -249,7 +276,7 @@ const Menu = () => { { icon: BarChart3, label: _('Dashboards'), - to: '/dashboards', + to: ROUTES.dashboards.url, key: 'dashboards', isPathMatch: Boolean(dashboardsMatch), active: isDashboardsActive, @@ -271,37 +298,37 @@ const Menu = () => { subNav: [ capabilities.mayAccess('task') && { label: _('Tasks'), - to: '/tasks', + to: ROUTES.tasks.url, isPathMatch: Boolean(tasksMatch), active: isTasksActive, }, capabilities.mayAccess('report') && { label: _('Reports'), - to: '/reports', + to: ROUTES.reports.url, isPathMatch: Boolean(reportsMatch), active: isReportsActive, }, capabilities.mayAccess('result') && { label: _('Results'), - to: '/results', + to: ROUTES.results.url, isPathMatch: Boolean(resultsMatch), active: isResultsActive, }, capabilities.mayAccess('vulnerability') && { label: _('Vulnerabilities'), - to: '/vulnerabilities', + to: ROUTES.vulnerabilities.url, isPathMatch: Boolean(vulnerabilitiesMatch), active: isVulnerabilitiesActive, }, capabilities.mayAccess('note') && { label: _('Notes'), - to: '/notes', + to: ROUTES.notes.url, isPathMatch: Boolean(notesMatch), active: isNotesActive, }, capabilities.mayAccess('override') && { label: _('Overrides'), - to: '/overrides', + to: ROUTES.overrides.url, isPathMatch: Boolean(overridesMatch), active: isOverridesActive, }, @@ -319,19 +346,19 @@ const Menu = () => { subNav: [ capabilities.mayAccess('host') && { label: _('Hosts'), - to: '/hosts', + to: ROUTES.hosts.url, isPathMatch: Boolean(hostsMatch), active: isHostsActive, }, capabilities.mayAccess('operatingsystem') && { label: _('Operating Systems'), - to: '/operating-systems', + to: ROUTES.operatingSystems.url, isPathMatch: Boolean(operatingSystemsMatch), active: isOperatingSystemsActive, }, capabilities.mayAccess('tlscertificate') && { label: _('TLS Certificates'), - to: '/tls-certificates', + to: ROUTES.tlsCertificates.url, isPathMatch: Boolean(tlsCertificatesMatch), active: isTlsCertificatesActive, }, @@ -350,25 +377,25 @@ const Menu = () => { subNav: [ capabilities.mayAccess('ticket') && { label: _('Remediation Tickets'), - to: '/tickets', + to: ROUTES.tickets.url, isPathMatch: Boolean(ticketsMatch), active: isTicketsActive, }, capabilities.mayAccess('policy') && { label: _('Compliance Policies'), - to: '/policies', + to: ROUTES.policies.url, isPathMatch: Boolean(policiesMatch), active: isPoliciesActive, }, capabilities.mayAccess('audit') && { label: _('Compliance Audits'), - to: '/audits', + to: ROUTES.audits.url, isPathMatch: Boolean(auditsMatch), active: isAuditsActive, }, capabilities.mayAccess('auditreport') && { label: _('Compliance Audit Reports'), - to: '/audit-reports', + to: ROUTES.auditReports.url, isPathMatch: Boolean(auditReportsMatch), active: isAuditReportsActive, }, @@ -388,31 +415,31 @@ const Menu = () => { subNav: [ { label: _('NVTs'), - to: '/nvts', + to: ROUTES.nvts.url, isPathMatch: Boolean(nvtsMatch), active: isNvtsActive, }, { label: _('CVEs'), - to: '/cves', + to: ROUTES.cves.url, isPathMatch: Boolean(cvesMatch), active: isCvesActive, }, { label: _('CPEs'), - to: '/cpes', + to: ROUTES.cpes.url, isPathMatch: Boolean(cpesMatch), active: isCpesActive, }, { label: _('CERT-Bund Advisories'), - to: '/cert-bund-advisories', + to: ROUTES.certBundAdvisories.url, isPathMatch: Boolean(certbundsMatch), active: isCertbundsActive, }, { label: _('DFN-CERT Advisories'), - to: '/dfn-cert-advisories', + to: ROUTES.dfnCertAdvisories.url, isPathMatch: Boolean(dfncertsMatch), active: isDfncertsActive, }, @@ -442,102 +469,102 @@ const Menu = () => { subNav: [ capabilities.mayAccess('target') && { label: _('Targets'), - to: '/targets', + to: ROUTES.targets.url, isPathMatch: Boolean(targetsMatch), active: isTargetsActive, }, capabilities.mayAccess('ociimagetarget') && features.featureEnabled('ENABLE_CONTAINER_SCANNING') && { label: _('Container Image Targets'), - to: '/oci-image-targets', + to: ROUTES.ociImageTargets.url, isPathMatch: Boolean(containerImageTargetsMatch), active: isContainerImageTargetsActive, }, capabilities.mayAccess('webapplicationtarget') && features.featureEnabled('ENABLE_WEB_APPLICATION_SCANNING') && { label: _('Web Application Targets'), - to: '/web-application-targets', + to: ROUTES.webApplicationTargets.url, isPathMatch: Boolean(webApplicationTargetsMatch), active: isWebApplicationTargetsActive, }, capabilities.mayAccess('portlist') && { label: _('Port Lists'), - to: '/port-lists', + to: ROUTES.portLists.url, isPathMatch: Boolean(portlistsMatch), active: isPortlistsActive, }, capabilities.mayAccess('credential') && { label: _('Credentials'), - to: '/credentials', + to: ROUTES.credentials.url, isPathMatch: Boolean(credentialsMatch), active: isCredentialsActive, }, capabilities.mayAccess('scanconfig') && { label: _('Scan Configs'), - to: '/scan-configs', + to: ROUTES.scanConfigs.url, isPathMatch: Boolean(scanConfigsMatch), active: isScanConfigsActive, }, capabilities.mayAccess('alert') && { label: _('Alerts'), - to: '/alerts', + to: ROUTES.alerts.url, isPathMatch: Boolean(alertsMatch), active: isAlertsActive, }, capabilities.mayAccess('schedule') && { label: _('Schedules'), - to: '/schedules', + to: ROUTES.schedules.url, isPathMatch: Boolean(schedulesMatch), active: isSchedulesActive, }, capabilities.mayAccess('reportconfig') && { label: _('Report Configs'), - to: '/report-configs', + to: ROUTES.reportConfigs.url, isPathMatch: Boolean(reportConfigsMatch), active: isReportConfigsActive, }, capabilities.mayAccess('reportformat') && { label: _('Report Formats'), - to: '/report-formats', + to: ROUTES.reportFormats.url, isPathMatch: Boolean(reportFormatsMatch), active: isReportFormatsActive, }, capabilities.mayAccess('scanner') && { label: _('Scanners'), - to: '/scanners', + to: ROUTES.scanners.url, isPathMatch: Boolean(scannersMatch), active: isScannersActive, }, capabilities.mayAccess('filter') && { label: _('Filters'), - to: '/filters', + to: ROUTES.filters.url, isPathMatch: Boolean(filtersMatch), active: isFiltersActive, }, capabilities.mayAccess('tag') && { label: _('Tags'), - to: '/tags', + to: ROUTES.tags.url, isPathMatch: Boolean(tagsMatch), active: isTagsActive, }, capabilities.mayAccess('agent') && features.featureEnabled('ENABLE_AGENTS') && { label: _('Agents'), - to: '/agents', + to: ROUTES.agents.url, isPathMatch: Boolean(agentsMatch), active: isAgentsActive, }, capabilities.mayAccess('agentgroup') && features.featureEnabled('ENABLE_AGENTS') && { label: _('Agent Groups'), - to: '/agent-groups', + to: ROUTES.agentGroups.url, isPathMatch: Boolean(agentGroupsMatch), active: isAgentGroupsActive, }, capabilities.mayOp('get_agent_installer_instruction') && features.featureEnabled('ENABLE_AGENTS') && { label: _('Agent Installers'), - to: '/agent-installers', + to: ROUTES.agentInstallers.url, isPathMatch: Boolean(agentInstallersMatch), active: isAgentInstallersActive, }, @@ -562,50 +589,50 @@ const Menu = () => { subNav: [ capabilities.mayAccess('user') && { label: _('Users'), - to: '/users', + to: ROUTES.users.url, isPathMatch: Boolean(usersMatch), active: isUserActive, }, capabilities.mayAccess('group') && { label: _('Groups'), - to: '/groups', + to: ROUTES.groups.url, isPathMatch: Boolean(groupsMatch), active: isGroupsActive, }, capabilities.mayAccess('role') && { label: _('Roles'), - to: '/roles', + to: ROUTES.roles.url, isPathMatch: Boolean(rolesMatch), active: isRolesActive, }, capabilities.mayAccess('permission') && { label: _('Permissions'), - to: '/permissions', + to: ROUTES.permissions.url, isPathMatch: Boolean(permissionsMatch), active: isPermissionsActive, }, capabilities.mayOp('get_system_reports') && { label: _('Performance'), - to: '/performance', + to: ROUTES.performance.url, isPathMatch: isPerformanceActive, active: isPerformanceActive, }, { label: _('Trashcan'), - to: '/trashcan', + to: ROUTES.trashcan.url, isPathMatch: isTrashcanActive, active: isTrashcanActive, }, capabilities.mayOp('get_feeds') && { label: _('Feed Status'), - to: '/feed-status', + to: ROUTES.feedStatus.url, isPathMatch: isFeedStatusActive, active: isFeedStatusActive, }, capabilities.mayOp('describe_auth') && capabilities.mayOp('modify_auth') && { label: _('LDAP'), - to: '/ldap', + to: ROUTES.ldap.url, isPathMatch: isLdapActive, active: isLdapActive, }, @@ -613,14 +640,14 @@ const Menu = () => { features.featureEnabled('ENABLE_CREDENTIAL_STORES') && capabilities.mayOp('modify_auth') && { label: _('Credential Store'), - to: '/credential-store', + to: ROUTES.credentialStore.url, isPathMatch: isCredentialStoreActive, active: isCredentialStoreActive, }, capabilities.mayOp('describe_auth') && capabilities.mayOp('modify_auth') && { label: _('RADIUS'), - to: '/radius', + to: ROUTES.radius.url, isPathMatch: isRadiusActive, active: isRadiusActive, }, @@ -634,7 +661,7 @@ const Menu = () => { subNav: [ { label: _('CVSS Calculator'), - to: '/cvss-calculator', + to: ROUTES.cvssCalculator.url, isPathMatch: isCvssCalculatorActive, active: isCvssCalculatorActive, }, diff --git a/src/web/components/structure/Header.tsx b/src/web/components/structure/Header.tsx index 6958f21635..a9cd1caf64 100644 --- a/src/web/components/structure/Header.tsx +++ b/src/web/components/structure/Header.tsx @@ -16,6 +16,7 @@ import useTranslation from 'web/hooks/useTranslation'; import useUserIsLoggedIn from 'web/hooks/useUserIsLoggedIn'; import useUserName from 'web/hooks/useUserName'; import useUserTimezone from 'web/hooks/useUserTimezone'; +import {ROUTES} from 'web/route-paths'; import {type ApplianceLogo} from 'web/utils/appliance-data'; import {saveLastVisitedPage} from 'web/utils/user-last-visited-page'; @@ -32,7 +33,7 @@ const Header = () => { const manualURL = useManualURL(); const handleSettingsClick = useCallback(async () => { - await navigate('/usersettings'); + await navigate(ROUTES.userSettings.url); }, [navigate]); const handleLogout = useCallback(() => { @@ -42,7 +43,7 @@ const Header = () => { } void gmp.doLogout().then(() => { - return navigate('/login?type=logout'); + return navigate(`${ROUTES.login.url}?type=logout`); }); }, [gmp, navigate, username, location]); @@ -66,7 +67,7 @@ const Header = () => { isThemeSwitchVisible={false} languageSwitch={} logo={logoComponent} - logoLink="/dashboards" + logoLink={ROUTES.dashboards.url} manualLink={manualURL} menuPoints={menuPoints} sessionTimer={} diff --git a/src/web/entity/Link.tsx b/src/web/entity/Link.tsx index c9102b30a0..efe5aee346 100644 --- a/src/web/entity/Link.tsx +++ b/src/web/entity/Link.tsx @@ -14,6 +14,7 @@ import DetailsLink from 'web/components/link/DetailsLink'; import Link, {type LinkProps} from 'web/components/link/Link'; import useCapabilities from 'web/hooks/useCapabilities'; import useTranslation from 'web/hooks/useTranslation'; +import {ROUTES} from 'web/route-paths'; interface EntityLinkProps extends LinkProps { entity: Model; @@ -30,7 +31,7 @@ const EntityLink = ({entity, textOnly, ...props}: EntityLinkProps) => { return ( {name} (in - + {_('Trashcan')} ) diff --git a/src/web/entity/__tests__/navigation.test.js b/src/web/entity/__tests__/navigation.test.js index 2c85957fdd..44d125f77b 100644 --- a/src/web/entity/__tests__/navigation.test.js +++ b/src/web/entity/__tests__/navigation.test.js @@ -9,52 +9,52 @@ import {goToDetails, goToList} from 'web/entity/navigation'; describe('goToDetails', () => { test('should return a function', () => { const navigate = testing.fn(); - goToDetails('type', navigate); + goToDetails('task', navigate); expect(goToDetails).toBeInstanceOf(Function); }); test('should throw an error if navigate is not provided', () => { - expect(() => goToDetails('type')).toThrow( + expect(() => goToDetails('task')).toThrow( 'navigate function is required for goToDetails', ); - expect(() => goToDetails('type', {})).toThrow( + expect(() => goToDetails('task', {})).toThrow( 'navigate function is required for goToDetails', ); - expect(() => goToDetails('type', {navigation: true})).toThrow( + expect(() => goToDetails('task', {navigation: true})).toThrow( 'navigate function is required for goToDetails', ); }); test('should navigate to the details page', () => { const navigate = testing.fn(); - const goToDetailsFn = goToDetails('type', navigate); + const goToDetailsFn = goToDetails('task', navigate); goToDetailsFn({data: {id: 1}}); - expect(navigate).toHaveBeenCalledWith('/type/1'); + expect(navigate).toHaveBeenCalledWith('/task/1'); }); }); describe('goToList', () => { test('should return a function', () => { const navigate = testing.fn(); - expect(goToList('type', navigate)).toBeInstanceOf(Function); + expect(goToList('task', navigate)).toBeInstanceOf(Function); }); test('should throw an error if navigate is not provided', () => { - expect(() => goToList('type')).toThrow( + expect(() => goToList('task')).toThrow( 'navigate function is required for goToList', ); - expect(() => goToList('type', {})).toThrow( + expect(() => goToList('task', {})).toThrow( 'navigate function is required for goToList', ); - expect(() => goToList('type', {navigation: true})).toThrow( + expect(() => goToList('task', {navigation: true})).toThrow( 'navigate function is required for goToList', ); }); test('should navigate to the list page', () => { const navigate = testing.fn(); - const goToListFn = goToList('type', navigate); + const goToListFn = goToList('task', navigate); goToListFn(); - expect(navigate).toHaveBeenCalledWith('/type'); + expect(navigate).toHaveBeenCalledWith('/tasks'); }); }); diff --git a/src/web/entity/navigation.ts b/src/web/entity/navigation.ts index a4df27b03d..86e5b39dd8 100644 --- a/src/web/entity/navigation.ts +++ b/src/web/entity/navigation.ts @@ -3,7 +3,9 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ +import {type EntityType} from 'gmp/utils/entity-type'; import {isDefined, isFunction} from 'gmp/utils/identity'; +import {entityListURL, entityURL, type EntityListType} from 'web/route-paths'; export type NavigateFunc = (path: string) => void; @@ -31,7 +33,7 @@ export type GotoListFunc = () => void; * @throws {Error} - Throws an error if the navigate function is not defined. */ export const goToDetails = ( - type: string, + type: EntityType, navigate: Navigate, ): GotoDetailsFunc => { if (!isDefined(navigate)) { @@ -45,7 +47,7 @@ export const goToDetails = ( throw new Error('navigate function is required for goToDetails'); } } - return ({data}) => navigate('/' + type + '/' + data.id); + return ({data}) => navigate(entityURL(type, String(data.id))); }; /** @@ -56,7 +58,10 @@ export const goToDetails = ( * @returns {GotoListFunc} A function that, when called, navigates to the specified list page. * @throws {Error} If the navigate function is not provided. */ -export const goToList = (type: string, navigate: Navigate): GotoListFunc => { +export const goToList = ( + type: EntityListType, + navigate: Navigate, +): GotoListFunc => { if (!isDefined(navigate)) { throw new Error('navigate function is required for goToList'); } @@ -68,5 +73,5 @@ export const goToList = (type: string, navigate: Navigate): GotoListFunc => { throw new Error('navigate function is required for goToList'); } } - return () => navigate('/' + type); + return () => navigate(entityListURL(type)); }; diff --git a/src/web/pages/OmpPage.tsx b/src/web/pages/OmpPage.tsx index e386e283ff..9de9ae2e24 100644 --- a/src/web/pages/OmpPage.tsx +++ b/src/web/pages/OmpPage.tsx @@ -5,6 +5,7 @@ import {useEffect} from 'react'; import {useNavigate, useSearchParams} from 'react-router'; +import {ROUTES} from 'web/route-paths'; /** * Component to redirect old secinfo urls like @@ -24,30 +25,32 @@ const OmpPage = () => { const infoId = searchParams.get('info_id') || ''; if (cmd !== 'get_info') { - void navigate('/notfound', {replace: true}); + void navigate(ROUTES.notFound.url, {replace: true}); return; } - const id = encodeURIComponent(infoId); - switch (infoType) { case 'nvt': - void navigate(`/nvt/${id}`, {replace: true}); + void navigate(ROUTES.nvt.url(infoId), {replace: true}); break; case 'cve': - void navigate(`/cve/${id}`, {replace: true}); + void navigate(ROUTES.cve.url(infoId), {replace: true}); break; case 'cpe': - void navigate(`/cpe/${id}`, {replace: true}); + void navigate(ROUTES.cpe.url(infoId), {replace: true}); break; case 'cert_bund_adv': - void navigate(`/certbund/${id}`, {replace: true}); + void navigate(ROUTES.certBundAdvisory.url(infoId), { + replace: true, + }); break; case 'dfn_cert_adv': - void navigate(`/dfncert/${id}`, {replace: true}); + void navigate(ROUTES.dfnCertAdvisory.url(infoId), { + replace: true, + }); break; default: - void navigate('/notfound', {replace: true}); + void navigate(ROUTES.notFound.url, {replace: true}); break; } }, [navigate, searchParams]); diff --git a/src/web/pages/__tests__/OmpPage.test.tsx b/src/web/pages/__tests__/OmpPage.test.tsx index a51f434489..fa7107296d 100644 --- a/src/web/pages/__tests__/OmpPage.test.tsx +++ b/src/web/pages/__tests__/OmpPage.test.tsx @@ -52,46 +52,46 @@ describe('OmpPage', () => { }); }); - test('should redirect cert_bund_adv info_type to /certbund/:id', async () => { + test('should redirect cert_bund_adv info_type to /cert-bund-advisory/:id', async () => { renderOmpPage( '?cmd=get_info&info_type=cert_bund_adv&info_id=CB-K21%2F0001', ); await waitFor(() => { expect(screen.getByTestId('location-pathname')).toHaveTextContent( - '/certbund/', + '/cert-bund-advisory/CB-K21%2F0001', ); }); }); - test('should redirect dfn_cert_adv info_type to /dfncert/:id', async () => { + test('should redirect dfn_cert_adv info_type to /dfn-cert-advisory/:id', async () => { renderOmpPage( '?cmd=get_info&info_type=dfn_cert_adv&info_id=DFN-CERT-2021-0001', ); await waitFor(() => { expect(screen.getByTestId('location-pathname')).toHaveTextContent( - '/dfncert/DFN-CERT-2021-0001', + '/dfn-cert-advisory/DFN-CERT-2021-0001', ); }); }); - test('should redirect to /notfound for an unknown info_type', async () => { + test('should redirect to /not-found for an unknown info_type', async () => { renderOmpPage('?cmd=get_info&info_type=unknown&info_id=foo'); await waitFor(() => { expect(screen.getByTestId('location-pathname')).toHaveTextContent( - '/notfound', + '/not-found', ); }); }); - test('should redirect to /notfound when cmd is not get_info', async () => { + test('should redirect to /not-found when cmd is not get_info', async () => { renderOmpPage('?cmd=other&info_type=nvt&info_id=foo'); await waitFor(() => { expect(screen.getByTestId('location-pathname')).toHaveTextContent( - '/notfound', + '/not-found', ); }); }); diff --git a/src/web/pages/alerts/DetailsPage.jsx b/src/web/pages/alerts/DetailsPage.jsx index 6397cb0e46..3d99c2be74 100644 --- a/src/web/pages/alerts/DetailsPage.jsx +++ b/src/web/pages/alerts/DetailsPage.jsx @@ -111,7 +111,7 @@ const Page = ({ onCloned={goToDetails('alert', props)} onCreated={goToDetails('alert', props)} onDeleteError={onError} - onDeleted={goToList('alerts', props)} + onDeleted={goToList('alert', props)} onDownloadError={onError} onDownloaded={onDownloaded} onSaved={onChanged} diff --git a/src/web/pages/audits/AuditDetailsPageToolBarIcons.tsx b/src/web/pages/audits/AuditDetailsPageToolBarIcons.tsx index 8d38f13f0b..d14c7c5114 100644 --- a/src/web/pages/audits/AuditDetailsPageToolBarIcons.tsx +++ b/src/web/pages/audits/AuditDetailsPageToolBarIcons.tsx @@ -22,6 +22,7 @@ import TaskResumeIcon from 'web/pages/tasks/icons/TaskResumeIcon'; import TaskScheduleIcon from 'web/pages/tasks/icons/TaskScheduleIcon'; import TaskStartIcon from 'web/pages/tasks/icons/TaskStartIcon'; import TaskStopIcon from 'web/pages/tasks/icons/TaskStopIcon'; +import {ROUTES} from 'web/route-paths'; import {formattedUserSettingShortDate} from 'web/utils/user-setting-time-date-formatters'; interface AuditDetailsPageToolBarIconsProps { @@ -144,7 +145,7 @@ const AuditDetailsPageToolBarIcons = ({ title={_('Total Reports for Audit {{- name}}', { name: entity.name as string, })} - to="auditreports" + to={ROUTES.auditReports.path} > @@ -157,7 +158,7 @@ const AuditDetailsPageToolBarIcons = ({ title={_('Results for Audit {{- name}}', { name: entity.name as string, })} - to="results" + to={ROUTES.results.url} > diff --git a/src/web/pages/audits/DetailsPage.tsx b/src/web/pages/audits/DetailsPage.tsx index d5af4fd6f0..24d1a61f0a 100644 --- a/src/web/pages/audits/DetailsPage.tsx +++ b/src/web/pages/audits/DetailsPage.tsx @@ -175,7 +175,7 @@ const AuditDetailsPage = () => { onCreateError={onError} onCreated={onChanged} onDeleteError={onError} - onDeleted={goToList('audits', navigate)} + onDeleted={goToList('audit', navigate)} onDownloadError={onError} onDownloaded={onDownloaded} onResumeError={onError} diff --git a/src/web/pages/audits/__tests__/AuditDetailsPageToolBarIcons.test.tsx b/src/web/pages/audits/__tests__/AuditDetailsPageToolBarIcons.test.tsx index d4990a94c9..ca82fcd104 100644 --- a/src/web/pages/audits/__tests__/AuditDetailsPageToolBarIcons.test.tsx +++ b/src/web/pages/audits/__tests__/AuditDetailsPageToolBarIcons.test.tsx @@ -210,7 +210,7 @@ describe('AuditDetailsPageToolBarIcons tests', () => { const totalReports = screen.getByTitle('Total Reports for Audit foo'); expect(totalReports).toHaveAttribute( 'href', - '/auditreports?filter=task_id%3D12345', + '/audit-reports?filter=task_id%3D12345', ); expect(within(totalReports).getByTestId('badge-icon')).toHaveTextContent( '0', @@ -317,7 +317,7 @@ describe('AuditDetailsPageToolBarIcons tests', () => { const totalReports = screen.getByTitle('Total Reports for Audit foo'); expect(totalReports).toHaveAttribute( 'href', - '/auditreports?filter=task_id%3D12345', + '/audit-reports?filter=task_id%3D12345', ); expect(within(totalReports).getByTestId('badge-icon')).toHaveTextContent( '1', @@ -423,7 +423,7 @@ describe('AuditDetailsPageToolBarIcons tests', () => { const totalReports = screen.getByTitle('Total Reports for Audit foo'); expect(totalReports).toHaveAttribute( 'href', - '/auditreports?filter=task_id%3D12345', + '/audit-reports?filter=task_id%3D12345', ); expect(within(totalReports).getByTestId('badge-icon')).toHaveTextContent( '2', @@ -526,7 +526,7 @@ describe('AuditDetailsPageToolBarIcons tests', () => { const totalReports = screen.getByTitle('Total Reports for Audit foo'); expect(totalReports).toHaveAttribute( 'href', - '/auditreports?filter=task_id%3D12345', + '/audit-reports?filter=task_id%3D12345', ); expect(within(totalReports).getByTestId('badge-icon')).toHaveTextContent( '1', @@ -637,7 +637,7 @@ describe('AuditDetailsPageToolBarIcons tests', () => { const totalReports = screen.getByTitle('Total Reports for Audit foo'); expect(totalReports).toHaveAttribute( 'href', - '/auditreports?filter=task_id%3D12345', + '/audit-reports?filter=task_id%3D12345', ); expect(within(totalReports).getByTestId('badge-icon')).toHaveTextContent( '1', diff --git a/src/web/pages/credentials/CredentialDetailsPage.tsx b/src/web/pages/credentials/CredentialDetailsPage.tsx index c563352b3b..84b76bec64 100644 --- a/src/web/pages/credentials/CredentialDetailsPage.tsx +++ b/src/web/pages/credentials/CredentialDetailsPage.tsx @@ -165,7 +165,7 @@ const CredentialDetailsPage = ({ onCloned={goToDetails('credential', navigate)} onCreated={goToDetails('credential', navigate)} onDeleteError={onError} - onDeleted={goToList('credentials', navigate)} + onDeleted={goToList('credential', navigate)} onDownloadError={onError} onDownloaded={onDownloaded} onInstallerDownloadError={onError} diff --git a/src/web/pages/cves/Details.jsx b/src/web/pages/cves/Details.jsx index 7c63d17185..ca222e7562 100644 --- a/src/web/pages/cves/Details.jsx +++ b/src/web/pages/cves/Details.jsx @@ -16,6 +16,7 @@ import TableData from 'web/components/table/TableData'; import TableRow from 'web/components/table/TableRow'; import DetailsBlock from 'web/entity/DetailsBlock'; import useGmp from 'web/hooks/useGmp'; +import {ROUTES} from 'web/route-paths'; import PropTypes from 'web/utils/prop-types'; import {renderPercentile, renderScore} from 'web/utils/severity'; @@ -66,7 +67,7 @@ const CveDetails = ({entity}) => { {cvssBaseVector} diff --git a/src/web/pages/cves/Row.jsx b/src/web/pages/cves/Row.jsx index d0e6190aa9..531dc218b9 100644 --- a/src/web/pages/cves/Row.jsx +++ b/src/web/pages/cves/Row.jsx @@ -14,6 +14,7 @@ import TableRow from 'web/components/table/TableRow'; import EntitiesActions from 'web/entities/EntitiesActions'; import RowDetailsToggle from 'web/entities/RowDetailsToggle'; import useGmp from 'web/hooks/useGmp'; +import {ROUTES} from 'web/route-paths'; import PropTypes from 'web/utils/prop-types'; import {renderPercentile, renderScore} from 'web/utils/severity'; @@ -41,7 +42,10 @@ const Row = ({ - + {entity.cvssBaseVector} diff --git a/src/web/pages/cves/__tests__/Row.test.jsx b/src/web/pages/cves/__tests__/Row.test.jsx index 7da91a34ab..4e1d879137 100644 --- a/src/web/pages/cves/__tests__/Row.test.jsx +++ b/src/web/pages/cves/__tests__/Row.test.jsx @@ -55,7 +55,7 @@ describe('CVEv2 Row tests', () => { expect(links[0]).toHaveAttribute( 'href', - '/cvsscalculator?cvssVector=AV%3AN%2FAC%3AM%2FAu%3AN%2FC%3AC%2FI%3AC%2FA%3AC', + '/cvss-calculator?cvssVector=AV%3AN%2FAC%3AM%2FAu%3AN%2FC%3AC%2FI%3AC%2FA%3AC', ); expect(links[0]).toHaveTextContent('AV:N/AC:M/Au:N/C:C/I:C/A:C'); @@ -136,7 +136,7 @@ describe('CVEv3 Row tests', () => { expect(links[0]).toHaveAttribute( 'href', - '/cvsscalculator?cvssVector=CVSS%3A3.1%2FAV%3AL%2FAC%3AL%2FPR%3AN%2FUI%3AR%2FS%3AU%2FC%3AN%2FI%3AH%2FA%3AH', + '/cvss-calculator?cvssVector=CVSS%3A3.1%2FAV%3AL%2FAC%3AL%2FPR%3AN%2FUI%3AR%2FS%3AU%2FC%3AN%2FI%3AH%2FA%3AH', ); expect(links[0]).toHaveTextContent( 'CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:H', diff --git a/src/web/pages/extras/FeedStatusPage.tsx b/src/web/pages/extras/FeedStatusPage.tsx index 52d8eef63e..3af1be2a48 100644 --- a/src/web/pages/extras/FeedStatusPage.tsx +++ b/src/web/pages/extras/FeedStatusPage.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import React, {useState} from 'react'; +import {useState} from 'react'; import { NVT_FEED, SCAP_FEED, @@ -42,6 +42,7 @@ import TableHead from 'web/components/table/TableHead'; import TableRow from 'web/components/table/TableRow'; import useGmp from 'web/hooks/useGmp'; import useTranslation from 'web/hooks/useTranslation'; +import {ROUTES} from 'web/route-paths'; interface FeedStatusProps { feeds: Feed[]; @@ -141,7 +142,7 @@ const FeedStatus = ({feeds}: FeedStatusProps) => { {feed.feedType === NVT_FEED && ( - + {_('NVTs')} @@ -151,13 +152,13 @@ const FeedStatus = ({feeds}: FeedStatusProps) => { )} {feed.feedType === SCAP_FEED && ( - + {_('CVEs')} - + {_('CPEs')} @@ -167,13 +168,13 @@ const FeedStatus = ({feeds}: FeedStatusProps) => { )} {feed.feedType === CERT_FEED && ( - + {_('CERT-Bund Advisories')} - + {_('DFN-CERT Advisories')} @@ -183,25 +184,31 @@ const FeedStatus = ({feeds}: FeedStatusProps) => { )} {feed.feedType === GVMD_DATA_FEED && ( - + {_('Compliance Policies')} - + {_('Port Lists')} - + {_('Report Formats')} - + {_('Scan Configs')} diff --git a/src/web/pages/extras/__tests__/FeedStatusPage.test.tsx b/src/web/pages/extras/__tests__/FeedStatusPage.test.tsx index c7fd2fda9c..869508207d 100644 --- a/src/web/pages/extras/__tests__/FeedStatusPage.test.tsx +++ b/src/web/pages/extras/__tests__/FeedStatusPage.test.tsx @@ -74,20 +74,20 @@ describe('Feed status page tests', () => { expect(links[1]).toHaveAttribute('href', '/nvts'); expect(links[2]).toHaveAttribute('href', '/cves'); expect(links[3]).toHaveAttribute('href', '/cpes'); - expect(links[4]).toHaveAttribute('href', '/certbunds'); - expect(links[5]).toHaveAttribute('href', '/dfncerts'); + expect(links[4]).toHaveAttribute('href', '/cert-bund-advisories'); + expect(links[5]).toHaveAttribute('href', '/dfn-cert-advisories'); expect(links[6]).toHaveAttribute('href', '/policies?filter=predefined%3D1'); expect(links[7]).toHaveAttribute( 'href', - '/portlists?filter=predefined%3D1', + '/port-lists?filter=predefined%3D1', ); expect(links[8]).toHaveAttribute( 'href', - '/reportformats?filter=predefined%3D1', + '/report-formats?filter=predefined%3D1', ); expect(links[9]).toHaveAttribute( 'href', - '/scanconfigs?filter=predefined%3D1', + '/scan-configs?filter=predefined%3D1', ); // Test headers diff --git a/src/web/pages/filters/DetailsPage.jsx b/src/web/pages/filters/DetailsPage.jsx index b73312a95b..fed6d6bea1 100644 --- a/src/web/pages/filters/DetailsPage.jsx +++ b/src/web/pages/filters/DetailsPage.jsx @@ -100,7 +100,7 @@ const Page = ({ onCloned={goToDetails('filter', props)} onCreated={goToDetails('filter', props)} onDeleteError={onError} - onDeleted={goToList('filters', props)} + onDeleted={goToList('filter', props)} onDownloadError={onError} onDownloaded={onDownloaded} onSaved={onChanged} diff --git a/src/web/pages/groups/DetailsPage.jsx b/src/web/pages/groups/DetailsPage.jsx index 3dd50b0a20..200dfe49b3 100644 --- a/src/web/pages/groups/DetailsPage.jsx +++ b/src/web/pages/groups/DetailsPage.jsx @@ -103,7 +103,7 @@ const Page = ({ onCloned={goToDetails('group', props)} onCreated={goToDetails('group', props)} onDeleteError={onError} - onDeleted={goToList('groups', props)} + onDeleted={goToList('group', props)} onDownloadError={onError} onDownloaded={onDownloaded} onSaved={onChanged} diff --git a/src/web/pages/hosts/DetailsPage.jsx b/src/web/pages/hosts/DetailsPage.jsx index 13964b3c22..5b49987e05 100644 --- a/src/web/pages/hosts/DetailsPage.jsx +++ b/src/web/pages/hosts/DetailsPage.jsx @@ -43,6 +43,7 @@ import withEntityContainer, { import useTranslation from 'web/hooks/useTranslation'; import HostDetails from 'web/pages/hosts/Details'; import HostComponent from 'web/pages/hosts/HostComponent'; +import {ROUTES} from 'web/route-paths'; import {selector as hostsSelector, loadEntity} from 'web/store/entities/hosts'; import { selector as permissionsSelector, @@ -94,14 +95,14 @@ export const ToolBarIcons = ({ @@ -253,7 +254,7 @@ const Page = ({ return ( { - void navigate(`/host/${data.id}`); + void navigate(ROUTES.host.url(data.id ?? '')); }; const severityRating = gmp.settings.severityRating; const { diff --git a/src/web/pages/login/LoginPageRoute.tsx b/src/web/pages/login/LoginPageRoute.tsx index 4f83ce9cd5..b259f87e7c 100644 --- a/src/web/pages/login/LoginPageRoute.tsx +++ b/src/web/pages/login/LoginPageRoute.tsx @@ -8,6 +8,7 @@ import Loading from 'web/components/loading/Loading'; import useUserIsLoggedIn from 'web/hooks/useUserIsLoggedIn'; import useUserName from 'web/hooks/useUserName'; import LoginPage from 'web/pages/login/LoginPage'; +import {ROUTES} from 'web/route-paths'; import { getLastVisitedPage, clearLastVisitedPage, @@ -23,10 +24,10 @@ const LoginPageRoute = () => { } if (isLoggedIn) { - let redirectPath = '/dashboards'; + let redirectPath: string = ROUTES.dashboards.url; if (username) { const lastVisited = getLastVisitedPage(username); - if (lastVisited && lastVisited !== '/login') { + if (lastVisited && lastVisited !== ROUTES.login.url) { redirectPath = lastVisited; clearLastVisitedPage(username); } diff --git a/src/web/pages/notes/NoteDetailsPage.tsx b/src/web/pages/notes/NoteDetailsPage.tsx index 6746ac2b6e..65a6c02b3b 100644 --- a/src/web/pages/notes/NoteDetailsPage.tsx +++ b/src/web/pages/notes/NoteDetailsPage.tsx @@ -132,7 +132,7 @@ const NoteDetailsPage = ({ onCloned={goToDetails('note', navigate)} onCreated={goToDetails('note', navigate)} onDeleteError={onError} - onDeleted={goToList('notes', navigate)} + onDeleted={goToList('note', navigate)} onDownloadError={onError} onDownloaded={onDownloaded} onSaved={onChanged} diff --git a/src/web/pages/nvts/NvtDetails.tsx b/src/web/pages/nvts/NvtDetails.tsx index a5942bc233..0d5e664c44 100644 --- a/src/web/pages/nvts/NvtDetails.tsx +++ b/src/web/pages/nvts/NvtDetails.tsx @@ -22,6 +22,7 @@ import useTranslation from 'web/hooks/useTranslation'; import NvtReferences from 'web/pages/nvts/NvtReferences'; import Pre from 'web/pages/nvts/Preformatted'; import Solution from 'web/pages/nvts/Solution'; +import {ROUTES} from 'web/route-paths'; import {na, getTranslatableSeverityOrigin} from 'web/utils/Render'; import {renderPercentile, renderScore} from 'web/utils/severity'; @@ -88,7 +89,7 @@ const NvtDetails = ({entity, links = true}: NvtDetailsProps) => { {tags.cvss_base_vector} @@ -239,7 +240,11 @@ const NvtDetails = ({entity, links = true}: NvtDetailsProps) => { {isDefined(family) && ( - + {family} diff --git a/src/web/pages/nvts/NvtDetailsPageToolBarIcons.tsx b/src/web/pages/nvts/NvtDetailsPageToolBarIcons.tsx index 1a023a1858..c274b7a91e 100644 --- a/src/web/pages/nvts/NvtDetailsPageToolBarIcons.tsx +++ b/src/web/pages/nvts/NvtDetailsPageToolBarIcons.tsx @@ -18,6 +18,7 @@ import IconDivider from 'web/components/layout/IconDivider'; import Link from 'web/components/link/Link'; import useCapabilities from 'web/hooks/useCapabilities'; import useTranslation from 'web/hooks/useTranslation'; +import {ROUTES} from 'web/route-paths'; interface NvtDetailsPageToolBarIconsProps { entity: Nvt; @@ -70,12 +71,12 @@ const NvtDetailsPageToolBarIcons = ({ {capabilities.mayAccess('result') && ( - + )} {capabilities.mayAccess('vulnerability') && ( - + )} diff --git a/src/web/pages/nvts/Row.jsx b/src/web/pages/nvts/Row.jsx index c38e1fec17..4c3621c4b5 100644 --- a/src/web/pages/nvts/Row.jsx +++ b/src/web/pages/nvts/Row.jsx @@ -18,6 +18,7 @@ import TableRow from 'web/components/table/TableRow'; import EntitiesActions from 'web/entities/EntitiesActions'; import RowDetailsToggle from 'web/entities/RowDetailsToggle'; import useGmp from 'web/hooks/useGmp'; +import {ROUTES} from 'web/route-paths'; import PropTypes from 'web/utils/prop-types'; import {renderPercentile, renderScore} from 'web/utils/severity'; @@ -51,7 +52,7 @@ const Row = ({ {entity.family} diff --git a/src/web/pages/operatingsystems/DetailsPage.jsx b/src/web/pages/operatingsystems/DetailsPage.jsx index 1e50326a6c..8631f761f3 100644 --- a/src/web/pages/operatingsystems/DetailsPage.jsx +++ b/src/web/pages/operatingsystems/DetailsPage.jsx @@ -40,6 +40,7 @@ import withEntityContainer, { import useCapabilities from 'web/hooks/useCapabilities'; import useTranslation from 'web/hooks/useTranslation'; import OsComponent from 'web/pages/operatingsystems/Component'; +import {ROUTES} from 'web/route-paths'; import { selector as osSelector, loadEntity, @@ -95,7 +96,7 @@ const ToolBarIcons = ({ filter={'os_id="' + entity.id + '"'} textOnly={!links} title={_('Hosts with Operating System {{- name}}', entity)} - to="hosts" + to={ROUTES.hosts.url} > @@ -110,7 +111,7 @@ const ToolBarIcons = ({ 'Hosts with Operating System {{- name}} as the best match', entity, )} - to="hosts" + to={ROUTES.hosts.url} > @@ -192,7 +193,7 @@ const Page = ({ return ( diff --git a/src/web/pages/operatingsystems/dashboard/OperatingSystemVulnerabilityScoreDisplay.tsx b/src/web/pages/operatingsystems/dashboard/OperatingSystemVulnerabilityScoreDisplay.tsx index 6d054057c8..7a4f680085 100644 --- a/src/web/pages/operatingsystems/dashboard/OperatingSystemVulnerabilityScoreDisplay.tsx +++ b/src/web/pages/operatingsystems/dashboard/OperatingSystemVulnerabilityScoreDisplay.tsx @@ -25,6 +25,7 @@ import { type OperatingSystemVulnerabilityScoreData, OperatingSystemVulnerabilityScoreLoader, } from 'web/pages/operatingsystems/dashboard/OperatingSystemLoaders'; +import {ROUTES} from 'web/route-paths'; import {NA_VALUE, resultSeverityRiskFactor} from 'web/utils/severity'; import {formattedUserSettingLongDate} from 'web/utils/user-setting-time-date-formatters'; @@ -133,7 +134,7 @@ export const OperatingSystemVulnerabilityScoreDisplay = ({ const displayFilter = showFilterSelection ? selectedFilter : filter; const handleDataClick = (data: TransformedVulnerabilityScoreDataItem) => { - void navigate(`/operatingsystem/${data.id}`); + void navigate(ROUTES.operatingSystem.url(data.id)); }; const severityRating = gmp.settings.severityRating; diff --git a/src/web/pages/overrides/OverrideDetailsPage.tsx b/src/web/pages/overrides/OverrideDetailsPage.tsx index 328b1b0d0c..f65dfbfcc5 100644 --- a/src/web/pages/overrides/OverrideDetailsPage.tsx +++ b/src/web/pages/overrides/OverrideDetailsPage.tsx @@ -132,7 +132,7 @@ const OverrideDetailsPage = ({ onCloned={goToDetails('override', navigate)} onCreated={goToDetails('override', navigate)} onDeleteError={onError} - onDeleted={goToList('overrides', navigate)} + onDeleted={goToList('override', navigate)} onDownloadError={onError} onDownloaded={onDownloaded} onSaved={onChanged} diff --git a/src/web/pages/permissions/PermissionDetailsPage.tsx b/src/web/pages/permissions/PermissionDetailsPage.tsx index 4700d841d1..09eb846c1a 100644 --- a/src/web/pages/permissions/PermissionDetailsPage.tsx +++ b/src/web/pages/permissions/PermissionDetailsPage.tsx @@ -101,7 +101,7 @@ const PermissionsDetailsPage = ({ onCloned={goToDetails('permission', navigate)} onCreated={goToDetails('permission', navigate)} onDeleteError={onError} - onDeleted={goToList('permissions', navigate)} + onDeleted={goToList('permission', navigate)} onDownloadError={onError} onDownloaded={onDownloaded} onSaved={onChanged} diff --git a/src/web/pages/policies/DetailsPage.tsx b/src/web/pages/policies/DetailsPage.tsx index ac410d6ac7..685e169bca 100644 --- a/src/web/pages/policies/DetailsPage.tsx +++ b/src/web/pages/policies/DetailsPage.tsx @@ -85,7 +85,7 @@ const PolicyDetailsPage = () => { onCloneError={onError} onCloned={goToDetails('policy', navigate)} onDeleteError={onError} - onDeleted={goToList('policies', navigate)} + onDeleted={goToList('policy', navigate)} onDownloadError={onError} onDownloaded={onDownloaded} onSaved={onChanged} diff --git a/src/web/pages/portlists/PortListDetailsPage.tsx b/src/web/pages/portlists/PortListDetailsPage.tsx index f141fa1a25..a1bb72fe5d 100644 --- a/src/web/pages/portlists/PortListDetailsPage.tsx +++ b/src/web/pages/portlists/PortListDetailsPage.tsx @@ -78,7 +78,7 @@ const PortListDetailsPage = ({ onCloned={goToDetails('portlist', navigate)} onCreated={goToDetails('portlist', navigate)} onDeleteError={onError} - onDeleted={goToList('portlists', navigate)} + onDeleted={goToList('portlist', navigate)} onDownloadError={onError} onDownloaded={onDownloaded} onSaved={onChanged} diff --git a/src/web/pages/reportconfigs/DetailsPage.jsx b/src/web/pages/reportconfigs/DetailsPage.jsx index a0afafd035..6dc49c3ac8 100644 --- a/src/web/pages/reportconfigs/DetailsPage.jsx +++ b/src/web/pages/reportconfigs/DetailsPage.jsx @@ -123,7 +123,7 @@ const Page = ({ onCreateError={onError} onCreated={goToDetails('reportconfig', props)} onDeleteError={onError} - onDeleted={goToList('reportconfigs', props)} + onDeleted={goToList('reportconfig', props)} onSaveError={onError} onSaved={onChanged} > diff --git a/src/web/pages/reportformats/DetailsPage.jsx b/src/web/pages/reportformats/DetailsPage.jsx index f190f7cef3..36fd45f310 100644 --- a/src/web/pages/reportformats/DetailsPage.jsx +++ b/src/web/pages/reportformats/DetailsPage.jsx @@ -211,7 +211,7 @@ const Page = ({ return ( diff --git a/src/web/pages/reports/AuditReportsListPage.jsx b/src/web/pages/reports/AuditReportsListPage.jsx index f522fd0465..f7a29f8d04 100644 --- a/src/web/pages/reports/AuditReportsListPage.jsx +++ b/src/web/pages/reports/AuditReportsListPage.jsx @@ -26,6 +26,7 @@ import AuditReportsDashboard, { } from 'web/pages/reports/auditdashboard'; import AuditReportFilterDialog from 'web/pages/reports/AuditReportFilterDialog'; import AuditReportsTable from 'web/pages/reports/AuditReportTable'; +import {ROUTES} from 'web/route-paths'; import { loadEntities, selector as entitiesSelector, @@ -46,7 +47,7 @@ const ToolBarIcons = () => { }; export const getAuditDeltaReportPath = (reportId, deltaReportId) => - `/audit-report/delta/${reportId}/${deltaReportId}`; + ROUTES.auditReportDelta.url(reportId, deltaReportId); const AuditReportListPage = ({filter, onFilterChanged, onDelete, ...props}) => { const [selectedDeltaReport, setSelectedDeltaReport] = useState(); diff --git a/src/web/pages/reports/ReportListPage.tsx b/src/web/pages/reports/ReportListPage.tsx index 64a6499b10..6b0b8f5b05 100644 --- a/src/web/pages/reports/ReportListPage.tsx +++ b/src/web/pages/reports/ReportListPage.tsx @@ -37,6 +37,7 @@ import ReportImportDialog, { } from 'web/pages/reports/ReportImportDialog'; import ReportsTable from 'web/pages/reports/ReportTable'; import ImportTaskDialog from 'web/pages/tasks/ImportTaskDialog'; +import {ROUTES} from 'web/route-paths'; import { loadEntities, selector as entitiesSelector, @@ -173,7 +174,7 @@ const ReportListPage = ({ if (isDefined(selectedDeltaReport)) { isDefined(onFilterChanged) && onFilterChanged(beforeSelectFilter as FilterType); - void navigate(`/report/delta/${selectedDeltaReport.id}/${report.id}`, { + void navigate(ROUTES.reportDelta.url(selectedDeltaReport.id, report.id), { replace: true, }); } else { diff --git a/src/web/pages/reports/__tests__/AuditDeltaDetailsPage.test.jsx b/src/web/pages/reports/__tests__/AuditDeltaDetailsPage.test.jsx index 9b8fdb275b..fdeac13bcf 100644 --- a/src/web/pages/reports/__tests__/AuditDeltaDetailsPage.test.jsx +++ b/src/web/pages/reports/__tests__/AuditDeltaDetailsPage.test.jsx @@ -121,7 +121,7 @@ describe('AuditDeltaDetailsContent tests', () => { ); expect(screen.getByTestId('list-link-icon')).toHaveAttribute( 'href', - '/auditreports', + '/audit-reports', ); expect(screen.getByTitle(/^Add to Assets/)).toBeInTheDocument(); expect(screen.getByTitle(/^Remove from Assets/)).toBeInTheDocument(); diff --git a/src/web/pages/reports/__tests__/AuditReportDetailsContent.test.tsx b/src/web/pages/reports/__tests__/AuditReportDetailsContent.test.tsx index fed3328af0..fc799e9706 100644 --- a/src/web/pages/reports/__tests__/AuditReportDetailsContent.test.tsx +++ b/src/web/pages/reports/__tests__/AuditReportDetailsContent.test.tsx @@ -314,7 +314,7 @@ describe('AuditReportDetailsContent tests', () => { ); expect(screen.getByTestId('list-link-icon')).toHaveAttribute( 'href', - '/auditreports', + '/audit-reports', ); expect(screen.getByTitle(/^Add to Assets/)).toBeInTheDocument(); expect(screen.getByTitle(/^Remove from Assets/)).toBeInTheDocument(); diff --git a/src/web/pages/reports/__tests__/ReportDetailsContent.test.tsx b/src/web/pages/reports/__tests__/ReportDetailsContent.test.tsx index a87db25bd1..3cc781a031 100644 --- a/src/web/pages/reports/__tests__/ReportDetailsContent.test.tsx +++ b/src/web/pages/reports/__tests__/ReportDetailsContent.test.tsx @@ -437,7 +437,7 @@ describe('ReportDetailsContent tests', () => { ).toHaveAttribute('href', '/vulnerabilities?filter=report_id%3D1234'); expect( screen.getByTitle(/^Corresponding TLS Certificates/), - ).toHaveAttribute('href', '/tlscertificates?filter=report_id%3D1234'); + ).toHaveAttribute('href', '/tls-certificates?filter=report_id%3D1234'); }); test('should render asset action buttons', () => { diff --git a/src/web/pages/reports/details/ReportDetailsPageToolBarIcons.tsx b/src/web/pages/reports/details/ReportDetailsPageToolBarIcons.tsx index 72478cce64..912f4a4f19 100644 --- a/src/web/pages/reports/details/ReportDetailsPageToolBarIcons.tsx +++ b/src/web/pages/reports/details/ReportDetailsPageToolBarIcons.tsx @@ -26,6 +26,7 @@ import DetailsLink from 'web/components/link/DetailsLink'; import Link from 'web/components/link/Link'; import useTranslation from 'web/hooks/useTranslation'; import AlertActions from 'web/pages/reports/details/AlertActions'; +import {ROUTES} from 'web/route-paths'; interface ReportDetailsToolBarIconsProps { audit?: boolean; @@ -74,7 +75,10 @@ const ReportDetailsToolBarIcons = ({ page="compliance-and-special-scans" title={_('Help: Audit Reports')} /> - + ) : ( <> @@ -118,7 +122,7 @@ const ReportDetailsToolBarIcons = ({ @@ -126,7 +130,7 @@ const ReportDetailsToolBarIcons = ({ @@ -135,7 +139,7 @@ const ReportDetailsToolBarIcons = ({ @@ -155,7 +159,7 @@ const ReportDetailsToolBarIcons = ({ ...(isDefined(report?.slave) && {scanner: report.slave.id}), }} title={_('Corresponding Performance')} - to="performance" + to={ROUTES.performance.url} > diff --git a/src/web/pages/reports/details/__tests__/OperatingSystemsTab.test.tsx b/src/web/pages/reports/details/__tests__/OperatingSystemsTab.test.tsx index 21d2685e16..6f891e6bde 100644 --- a/src/web/pages/reports/details/__tests__/OperatingSystemsTab.test.tsx +++ b/src/web/pages/reports/details/__tests__/OperatingSystemsTab.test.tsx @@ -138,12 +138,12 @@ describe('Report Operating Systems Tab tests', () => { expect(row1Links[0]).toHaveTextContent('Foo OS'); expect(row1Links[0]).toHaveAttribute( 'href', - '/operatingsystems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', + '/operating-systems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', ); expect(row1Links[1]).toHaveTextContent('cpe:/foo/bar'); expect(row1Links[1]).toHaveAttribute( 'href', - '/operatingsystems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', + '/operating-systems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', ); // Verify Row 2 @@ -154,12 +154,12 @@ describe('Report Operating Systems Tab tests', () => { expect(row2Links[0]).toHaveTextContent('Lorem OS'); expect(row2Links[0]).toHaveAttribute( 'href', - '/operatingsystems?filter=name%3Dcpe%3A%2Florem%2Fipsum', + '/operating-systems?filter=name%3Dcpe%3A%2Florem%2Fipsum', ); expect(row2Links[1]).toHaveTextContent('cpe:/lorem/ipsum'); expect(row2Links[1]).toHaveAttribute( 'href', - '/operatingsystems?filter=name%3Dcpe%3A%2Florem%2Fipsum', + '/operating-systems?filter=name%3Dcpe%3A%2Florem%2Fipsum', ); }); @@ -248,7 +248,7 @@ describe('Audit Report Operating Systems Tab tests', () => { expect(row1Links[0]).toHaveTextContent('Foo OS'); expect(row1Links[0]).toHaveAttribute( 'href', - '/operatingsystems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', + '/operating-systems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', ); expect(row1Links[1]).toHaveTextContent('cpe:/foo/bar'); expect(bars[0]).toHaveAttribute('title', 'No'); @@ -262,7 +262,7 @@ describe('Audit Report Operating Systems Tab tests', () => { expect(row2Links[0]).toHaveTextContent('Lorem OS'); expect(row2Links[0]).toHaveAttribute( 'href', - '/operatingsystems?filter=name%3Dcpe%3A%2Florem%2Fipsum', + '/operating-systems?filter=name%3Dcpe%3A%2Florem%2Fipsum', ); expect(row2Links[1]).toHaveTextContent('cpe:/lorem/ipsum'); expect(bars[1]).toHaveAttribute('title', 'Incomplete'); diff --git a/src/web/pages/reports/details/__tests__/OperatingSystemsTable.test.tsx b/src/web/pages/reports/details/__tests__/OperatingSystemsTable.test.tsx index 60739c8bcf..a5e82fdd35 100644 --- a/src/web/pages/reports/details/__tests__/OperatingSystemsTable.test.tsx +++ b/src/web/pages/reports/details/__tests__/OperatingSystemsTable.test.tsx @@ -96,7 +96,7 @@ describe('OperatingSystemsTable', () => { expect(osLink.closest('a')).toHaveAttribute( 'href', - '/operatingsystems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', + '/operating-systems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', ); }); @@ -121,7 +121,7 @@ describe('OperatingSystemsTable', () => { expect(cpeLink.closest('a')).toHaveAttribute( 'href', - '/operatingsystems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', + '/operating-systems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', ); }); diff --git a/src/web/pages/reports/details/__tests__/ReportDetailsPageToolbarIcons.test.tsx b/src/web/pages/reports/details/__tests__/ReportDetailsPageToolbarIcons.test.tsx index 7bc73d1f09..4f67da7758 100644 --- a/src/web/pages/reports/details/__tests__/ReportDetailsPageToolbarIcons.test.tsx +++ b/src/web/pages/reports/details/__tests__/ReportDetailsPageToolbarIcons.test.tsx @@ -117,7 +117,7 @@ describe('Report Details ToolBarIcons tests', () => { expect(links[5]).toHaveAttribute('title', 'Corresponding TLS Certificates'); expect(links[5]).toHaveAttribute( 'href', - '/tlscertificates?filter=report_id%3D1234', + '/tls-certificates?filter=report_id%3D1234', ); // Corresponding Performance Icon diff --git a/src/web/pages/reports/details/cve/ClosedCvesTable.tsx b/src/web/pages/reports/details/cve/ClosedCvesTable.tsx index ab3adfb128..448b3cb0a0 100644 --- a/src/web/pages/reports/details/cve/ClosedCvesTable.tsx +++ b/src/web/pages/reports/details/cve/ClosedCvesTable.tsx @@ -15,6 +15,7 @@ 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 {ROUTES} from 'web/route-paths'; import {type SortDirectionType} from 'web/utils/sort-direction'; interface HeaderProps { @@ -47,7 +48,7 @@ const getColumns = () => [ ) : ( - + {entity.host.ip} ), diff --git a/src/web/pages/reports/details/cve/CvesTable.tsx b/src/web/pages/reports/details/cve/CvesTable.tsx index d0340e75c5..4a0f3ccf35 100644 --- a/src/web/pages/reports/details/cve/CvesTable.tsx +++ b/src/web/pages/reports/details/cve/CvesTable.tsx @@ -15,6 +15,7 @@ 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 {ROUTES} from 'web/route-paths'; import {type SortDirectionType} from 'web/utils/sort-direction'; interface HeaderProps { @@ -47,7 +48,7 @@ const getColumns = () => [ ) : ( - + {entity.host.ip} ), diff --git a/src/web/pages/reports/details/host/AgentScanningHostsTable.tsx b/src/web/pages/reports/details/host/AgentScanningHostsTable.tsx index 5be5dccf4b..2645117d5b 100644 --- a/src/web/pages/reports/details/host/AgentScanningHostsTable.tsx +++ b/src/web/pages/reports/details/host/AgentScanningHostsTable.tsx @@ -24,6 +24,7 @@ import TableHeader from 'web/components/table/TableHeader'; import TableRow from 'web/components/table/TableRow'; import createEntitiesTable from 'web/entities/createEntitiesTable'; import useGmp from 'web/hooks/useGmp'; +import {ROUTES} from 'web/route-paths'; import {type SortDirectionType} from 'web/utils/sort-direction'; interface HeaderProps { @@ -89,7 +90,7 @@ const getColumns = (audit = false, useCVSSv3 = false) => [ {ip} ) : ( - + {ip} )} diff --git a/src/web/pages/reports/details/host/HostsTable.tsx b/src/web/pages/reports/details/host/HostsTable.tsx index a9c3d188e1..8f8f827fb2 100644 --- a/src/web/pages/reports/details/host/HostsTable.tsx +++ b/src/web/pages/reports/details/host/HostsTable.tsx @@ -25,6 +25,7 @@ import TableHeader from 'web/components/table/TableHeader'; import TableRow from 'web/components/table/TableRow'; import createEntitiesTable from 'web/entities/createEntitiesTable'; import useGmp from 'web/hooks/useGmp'; +import {ROUTES} from 'web/route-paths'; import {type SortDirectionType} from 'web/utils/sort-direction'; interface HeaderProps { @@ -90,7 +91,7 @@ const getColumns = (audit = false, useCVSSv3 = false) => [ {ip} ) : ( - + {ip} )} diff --git a/src/web/pages/reports/details/operating-system/OperatingSystemsTable.tsx b/src/web/pages/reports/details/operating-system/OperatingSystemsTable.tsx index 37fd8ed011..8a6c46e105 100644 --- a/src/web/pages/reports/details/operating-system/OperatingSystemsTable.tsx +++ b/src/web/pages/reports/details/operating-system/OperatingSystemsTable.tsx @@ -14,6 +14,7 @@ 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 {ROUTES} from 'web/route-paths'; import {type SortDirectionType} from 'web/utils/sort-direction'; interface HeaderProps { @@ -36,7 +37,7 @@ const getColumns = (audit = false) => [ @@ -58,7 +59,7 @@ const getColumns = (audit = false) => [ {entity.cpe} diff --git a/src/web/pages/reports/details/tls-certificate/TlsCertificatesTable.tsx b/src/web/pages/reports/details/tls-certificate/TlsCertificatesTable.tsx index d1d6b2de85..df55cb8eac 100644 --- a/src/web/pages/reports/details/tls-certificate/TlsCertificatesTable.tsx +++ b/src/web/pages/reports/details/tls-certificate/TlsCertificatesTable.tsx @@ -22,6 +22,7 @@ import { import RowDetailsToggle from 'web/entities/RowDetailsToggle'; import withRowDetails from 'web/entities/withRowDetails'; import TlsCertificateDetails from 'web/pages/tlscertificates/Details'; +import {ROUTES} from 'web/route-paths'; import {type SortDirectionType} from 'web/utils/sort-direction'; interface HeaderProps { @@ -113,7 +114,7 @@ const getColumns = ({ filter={'name=' + entity.ip} textOnly={!links} title={_('Show all Hosts with IP {{ip}}', {ip: entity.ip ?? ''})} - to="hosts" + to={ROUTES.hosts.url} > {entity.ip} diff --git a/src/web/pages/results/DetailsPage.jsx b/src/web/pages/results/DetailsPage.jsx index c543fe4219..3ee808e98b 100644 --- a/src/web/pages/results/DetailsPage.jsx +++ b/src/web/pages/results/DetailsPage.jsx @@ -42,6 +42,7 @@ import OverrideComponent from 'web/pages/overrides/OverrideComponent'; import ResultDetails from 'web/pages/results/ResultDetails'; import ResultDetailsPageToolBarIcons from 'web/pages/results/ResultDetailsPageToolBarIcons'; import TicketComponent from 'web/pages/tickets/TicketComponent'; +import {ROUTES} from 'web/route-paths'; import {loadEntity, selector} from 'web/store/entities/results'; import {loadUserSettingDefaults} from 'web/store/usersettings/defaults/actions'; import {getUserSettingsDefaults} from 'web/store/usersettings/defaults/selectors'; @@ -87,7 +88,7 @@ const Details = ({entity, ...props}) => { {active_overrides.length > 0 && ( - + diff --git a/src/web/pages/results/ResultDetailsPageToolBarIcons.tsx b/src/web/pages/results/ResultDetailsPageToolBarIcons.tsx index ce3dfc18eb..f8a0844c21 100644 --- a/src/web/pages/results/ResultDetailsPageToolBarIcons.tsx +++ b/src/web/pages/results/ResultDetailsPageToolBarIcons.tsx @@ -23,6 +23,7 @@ import DetailsLink from 'web/components/link/DetailsLink'; import Link from 'web/components/link/Link'; import useCapabilities from 'web/hooks/useCapabilities'; import useTranslation from 'web/hooks/useTranslation'; +import {ROUTES} from 'web/route-paths'; interface ResultDetailsPageToolBarIconsProps { entity: Result; @@ -109,7 +110,7 @@ const ResultDetailsPageToolBarIcons = ({ diff --git a/src/web/pages/roles/RoleDetailsPage.tsx b/src/web/pages/roles/RoleDetailsPage.tsx index fd7ee6db33..c8675976c7 100644 --- a/src/web/pages/roles/RoleDetailsPage.tsx +++ b/src/web/pages/roles/RoleDetailsPage.tsx @@ -135,7 +135,7 @@ const RoleDetailsPage = ({ onCloned={goToDetails('role', navigate)} onCreated={goToDetails('role', navigate)} onDeleteError={onError} - onDeleted={goToList('roles', navigate)} + onDeleted={goToList('role', navigate)} onDownloadError={onError} onDownloaded={onDownloaded} onSaved={onChanged} diff --git a/src/web/pages/scanconfigs/DetailsPage.jsx b/src/web/pages/scanconfigs/DetailsPage.jsx index 6ed1f4847b..592e16ecf5 100644 --- a/src/web/pages/scanconfigs/DetailsPage.jsx +++ b/src/web/pages/scanconfigs/DetailsPage.jsx @@ -37,6 +37,7 @@ import ScanConfigDetails from 'web/pages/scanconfigs/Details'; import ScanConfigComponent from 'web/pages/scanconfigs/ScanConfigComponent'; import ScanConfigDetailsPageToolBarIcons from 'web/pages/scanconfigs/ScanConfigDetailsPageToolBarIcons'; import ScanConfigTrend from 'web/pages/scanconfigs/ScanConfigTrend'; +import {ROUTES} from 'web/route-paths'; import { selector as permissionsSelector, loadEntities as loadPermissions, @@ -84,7 +85,7 @@ export const NvtFamilies = ({entity}) => { title={_('NVTs of family {{name}}', { name: family.name, })} - to="nvts" + to={ROUTES.nvts.url} > {family.name} @@ -231,7 +232,7 @@ const Page = ({ onCloned={goToDetails('scanconfig', props)} onCreated={goToDetails('scanconfig', props)} onDeleteError={onError} - onDeleted={goToList('scanconfigs', props)} + onDeleted={goToList('scanconfig', props)} onDownloadError={onError} onDownloaded={onDownloaded} onImported={goToDetails('scanconfig', props)} diff --git a/src/web/pages/scanconfigs/ScanConfigEditNvtDetailsDialog.tsx b/src/web/pages/scanconfigs/ScanConfigEditNvtDetailsDialog.tsx index c877fddf00..11ec46dcf7 100644 --- a/src/web/pages/scanconfigs/ScanConfigEditNvtDetailsDialog.tsx +++ b/src/web/pages/scanconfigs/ScanConfigEditNvtDetailsDialog.tsx @@ -33,6 +33,7 @@ import TableRow from 'web/components/table/TableRow'; import useTranslation from 'web/hooks/useTranslation'; import NvtPreference from 'web/pages/nvts/NvtPreference'; import Preformatted from 'web/pages/nvts/Preformatted'; +import {ROUTES} from 'web/route-paths'; export interface ScanConfigEditNvtDetailsDialogData { configId: string; @@ -257,7 +258,7 @@ const ScanConfigEditNvtDetailsDialog = ({ {nvtCvssVector} diff --git a/src/web/pages/scanners/ScannerDetailsPage.tsx b/src/web/pages/scanners/ScannerDetailsPage.tsx index 603752b022..2030de476d 100644 --- a/src/web/pages/scanners/ScannerDetailsPage.tsx +++ b/src/web/pages/scanners/ScannerDetailsPage.tsx @@ -78,7 +78,7 @@ const ScannerDetailsPage = ({ onCredentialDownloadError={onError} onCredentialDownloaded={onDownloaded} onDeleteError={onError} - onDeleted={goToList('scanners', navigate)} + onDeleted={goToList('scanner', navigate)} onDownloadError={onError} onDownloaded={onDownloaded} onSaved={onChanged} diff --git a/src/web/pages/schedules/DetailsPage.jsx b/src/web/pages/schedules/DetailsPage.jsx index 23097f118b..73adb50967 100644 --- a/src/web/pages/schedules/DetailsPage.jsx +++ b/src/web/pages/schedules/DetailsPage.jsx @@ -48,7 +48,7 @@ const Page = ({ onCloned={goToDetails('schedule', props)} onCreated={goToDetails('schedule', props)} onDeleteError={onError} - onDeleted={goToList('schedules', props)} + onDeleted={goToList('schedule', props)} onDownloadError={onError} onDownloaded={onDownloaded} onSaved={onChanged} diff --git a/src/web/pages/tags/TagDetailsPage.tsx b/src/web/pages/tags/TagDetailsPage.tsx index 744f03fc6b..e53d536734 100644 --- a/src/web/pages/tags/TagDetailsPage.tsx +++ b/src/web/pages/tags/TagDetailsPage.tsx @@ -150,7 +150,7 @@ const TagDetailsPage = () => { onCloned={goToDetails('tag', navigate)} onCreated={goToDetails('tag', navigate)} onDeleteError={onError} - onDeleted={goToList('tags', navigate)} + onDeleted={goToList('tag', navigate)} onDisableError={onError} onDisabled={onChanged} onDownloadError={onError} diff --git a/src/web/pages/targets/TargetDetailsPage.tsx b/src/web/pages/targets/TargetDetailsPage.tsx index 0beedf7225..9a23eec928 100644 --- a/src/web/pages/targets/TargetDetailsPage.tsx +++ b/src/web/pages/targets/TargetDetailsPage.tsx @@ -110,7 +110,7 @@ const TargetDetailsPage = ({ onCloned={goToDetails('target', navigate)} onCreated={goToDetails('target', navigate)} onDeleteError={onError} - onDeleted={goToList('targets', navigate)} + onDeleted={goToList('target', navigate)} onDownloadError={onError} onDownloaded={onDownloaded} onSaved={onChanged} diff --git a/src/web/pages/tasks/TaskDetails.tsx b/src/web/pages/tasks/TaskDetails.tsx index c96fa22125..5b9064644e 100644 --- a/src/web/pages/tasks/TaskDetails.tsx +++ b/src/web/pages/tasks/TaskDetails.tsx @@ -26,6 +26,7 @@ import useFeatures from 'web/hooks/useFeatures'; import useGmp from 'web/hooks/useGmp'; import useShallowEqualSelector from 'web/hooks/useShallowEqualSelector'; import useTranslation from 'web/hooks/useTranslation'; +import {ROUTES} from 'web/route-paths'; import { loadEntity as loadScanConfig, selector as scanConfigSelector, @@ -145,7 +146,7 @@ const TaskDetails = ({entity, links = true}: TaskDetailsProps) => { {isContainerScanning && isDefined(ociImageTarget?.name) && ( - + {ociImageTarget.name} diff --git a/src/web/pages/tasks/TaskDetailsPage.tsx b/src/web/pages/tasks/TaskDetailsPage.tsx index 20b87e885f..fff7e0d54f 100644 --- a/src/web/pages/tasks/TaskDetailsPage.tsx +++ b/src/web/pages/tasks/TaskDetailsPage.tsx @@ -51,6 +51,7 @@ import TaskDetailsPageToolBarIcons from 'web/pages/tasks/icons/TaskDetailsPageTo import TaskComponent from 'web/pages/tasks/TaskComponent'; import TaskDetails from 'web/pages/tasks/TaskDetails'; import TaskStatus from 'web/pages/tasks/TaskStatus'; +import {ROUTES} from 'web/route-paths'; import { selector as notesSelector, loadEntities as loadNotes, @@ -156,7 +157,7 @@ const TaskDetailsPage = ({ useEffect(() => { if (isDefined(entity) && entity.usageType !== USAGE_TYPE.scan) { - void navigate(`/audit/${entity.id}`, {replace: true}); + void navigate(ROUTES.audit.url(entity.id), {replace: true}); } }, [entity, navigate]); @@ -166,7 +167,7 @@ const TaskDetailsPage = ({ onCloned={goToDetails('task', navigate)} onCreated={goToDetails('task', navigate)} onDeleteError={onError} - onDeleted={goToList('tasks', navigate)} + onDeleted={goToList('task', navigate)} onDownloadError={onError} onDownloaded={onDownloaded} onImportTaskCreated={goToDetails('task', navigate)} diff --git a/src/web/pages/tasks/__tests__/TaskDetails.test.tsx b/src/web/pages/tasks/__tests__/TaskDetails.test.tsx index 4d9cba750f..f121deb3f6 100644 --- a/src/web/pages/tasks/__tests__/TaskDetails.test.tsx +++ b/src/web/pages/tasks/__tests__/TaskDetails.test.tsx @@ -131,7 +131,7 @@ describe('TaskDetails tests', () => { const link = screen.getByText('my-container-image'); expect(link).toBeVisible(); - expect(link.closest('a')).toHaveAttribute('href', '/ociimagetargets'); + expect(link.closest('a')).toHaveAttribute('href', '/oci-image-targets'); }); test('should render full task details', () => { diff --git a/src/web/pages/tasks/icons/TaskDetailsPageToolBarIcons.tsx b/src/web/pages/tasks/icons/TaskDetailsPageToolBarIcons.tsx index babaf2c80c..de08cadb62 100644 --- a/src/web/pages/tasks/icons/TaskDetailsPageToolBarIcons.tsx +++ b/src/web/pages/tasks/icons/TaskDetailsPageToolBarIcons.tsx @@ -29,6 +29,7 @@ import TaskIconWithSync from 'web/pages/tasks/icons/TaskIconWithSync'; import TaskImportReportIcon from 'web/pages/tasks/icons/TaskImportReportIcon'; import TaskScheduleIcon from 'web/pages/tasks/icons/TaskScheduleIcon'; import TaskStopIcon from 'web/pages/tasks/icons/TaskStopIcon'; +import {ROUTES} from 'web/route-paths'; import {formattedUserSettingShortDate} from 'web/utils/user-setting-time-date-formatters'; interface TaskDetailsPageToolBarIconsProps { @@ -165,7 +166,7 @@ export const TaskDetailsPageToolBarIcons = ({ title={_('Total Reports for Task {{- name}}', { name: entity.name as string, })} - to="reports" + to={ROUTES.reports.url} > @@ -178,7 +179,7 @@ export const TaskDetailsPageToolBarIcons = ({ title={_('Results for Task {{- name}}', { name: entity.name as string, })} - to="results" + to={ROUTES.results.url} > @@ -191,7 +192,7 @@ export const TaskDetailsPageToolBarIcons = ({ title={_('Notes for Task {{- name}}', { name: entity.name as string, })} - to="notes" + to={ROUTES.notes.url} > @@ -203,7 +204,7 @@ export const TaskDetailsPageToolBarIcons = ({ title={_('Overrides for Task {{- name}}', { name: entity.name as string, })} - to="overrides" + to={ROUTES.overrides.url} > diff --git a/src/web/pages/tickets/TicketDetailsPage.tsx b/src/web/pages/tickets/TicketDetailsPage.tsx index c75284fb29..70c57b778f 100644 --- a/src/web/pages/tickets/TicketDetailsPage.tsx +++ b/src/web/pages/tickets/TicketDetailsPage.tsx @@ -203,7 +203,7 @@ const TicketDetailsPage = () => { onCloneError={onError} onCloned={handleEntityActionCompleted} onDeleteError={onError} - onDeleted={goToList('tickets', navigate)} + onDeleted={goToList('ticket', navigate)} onDownloadError={onError} onDownloaded={onDownloaded} onSaved={onChanged} diff --git a/src/web/pages/tlscertificates/DetailsPage.jsx b/src/web/pages/tlscertificates/DetailsPage.jsx index 07875679cc..10e465f1a1 100644 --- a/src/web/pages/tlscertificates/DetailsPage.jsx +++ b/src/web/pages/tlscertificates/DetailsPage.jsx @@ -204,7 +204,7 @@ const Page = ({ return ( diff --git a/src/web/pages/users/UserDetailsPage.tsx b/src/web/pages/users/UserDetailsPage.tsx index 30e257ef88..ef37e4ed9e 100644 --- a/src/web/pages/users/UserDetailsPage.tsx +++ b/src/web/pages/users/UserDetailsPage.tsx @@ -143,7 +143,7 @@ const UserDetailsPage = () => { onCloned={handleEntityActionCompleted} onCreated={handleEntityActionCompleted} onDeleteError={onError} - onDeleted={goToList('users', navigate)} + onDeleted={goToList('user', navigate)} onDialogError={onError} onDownloadError={onError} onDownloaded={onDownloaded} diff --git a/src/web/pages/vulnerabilities/VulnerabilitiesTableRow.tsx b/src/web/pages/vulnerabilities/VulnerabilitiesTableRow.tsx index 338d234d94..c761ecd14c 100644 --- a/src/web/pages/vulnerabilities/VulnerabilitiesTableRow.tsx +++ b/src/web/pages/vulnerabilities/VulnerabilitiesTableRow.tsx @@ -3,7 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import React from 'react'; import type Vulnerability from 'gmp/models/vulnerability'; import SeverityBar from 'web/components/bar/SeverityBar'; import DateTime from 'web/components/date/DateTime'; @@ -13,6 +12,7 @@ import Qod from 'web/components/qod/Qod'; import TableData from 'web/components/table/TableData'; import TableRow from 'web/components/table/TableRow'; import EntitiesActions from 'web/entities/EntitiesActions'; +import {ROUTES} from 'web/route-paths'; import {type SelectionTypeType} from 'web/utils/selection-type'; export interface VulnerabilitiesTableRowProps { @@ -54,7 +54,11 @@ const VulnerabilitiesTableRow = ({ - + {results.count} diff --git a/src/web/route-paths.ts b/src/web/route-paths.ts new file mode 100644 index 0000000000..9f35d350dc --- /dev/null +++ b/src/web/route-paths.ts @@ -0,0 +1,397 @@ +/* SPDX-FileCopyrightText: 2026 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {type EntityType} from 'gmp/utils/entity-type'; + +// Builds a concrete detail URL and encodes the entity ID as one URL segment. +const appendEncodedId = (path: string, id: string) => + `${path}/${encodeURIComponent(id)}`; + +// Converts a parameterized route pattern into a React Router wildcard match. +export const routeMatch = (path: string) => + `/${path.replace(/\/:([^/]+)/g, '/*')}`; + +/* + * Route descriptors keep router patterns and navigable URLs together: + * - path: relative React Router pattern; dynamic values use placeholders such as :id. + * - url: absolute URL used by links and navigation; dynamic values are encoded. + * - match: optional explicit active-menu pattern for routes with special matching needs. + * - legacy: old URL aliases kept for compatibility and redirected to canonical routes. + */ + +export const ROUTES = { + root: {path: '/', url: '/'}, + login: {path: 'login', url: '/login'}, + omp: {path: 'omp', url: '/omp'}, + dashboards: {path: 'dashboards', url: '/dashboards'}, + agentInstallers: {path: 'agent-installers', url: '/agent-installers'}, + agents: {path: 'agents', url: '/agents'}, + agentGroups: {path: 'agent-groups', url: '/agent-groups'}, + agent: { + path: 'agent/:id', + url: (id: string) => appendEncodedId('/agent', id), + }, + agentGroup: { + path: 'agent-group/:id', + url: (id: string) => appendEncodedId('/agent-group', id), + }, + agentInstaller: { + path: 'agent-installer/:id', + url: (id: string) => appendEncodedId('/agent-installer', id), + }, + alerts: {path: 'alerts', url: '/alerts'}, + alert: { + path: 'alert/:id', + url: (id: string) => appendEncodedId('/alert', id), + }, + audits: {path: 'audits', url: '/audits'}, + audit: { + path: 'audit/:id', + url: (id: string) => appendEncodedId('/audit', id), + }, + auditReports: { + path: 'audit-reports', + // Keep this explicit while the menu uses the canonical list path directly. + match: '/audit-reports', + url: '/audit-reports', + }, + auditReport: { + path: 'audit-report/:id', + // The detail route needs a wildcard match for active-menu state. + match: '/audit-report/*', + url: (id: string) => appendEncodedId('/audit-report', id), + }, + auditReportDelta: { + path: 'audit-report/delta/:id/:deltaid', + url: (id: string, deltaId: string) => + `${appendEncodedId('/audit-report/delta', id)}/${encodeURIComponent(deltaId)}`, + }, + reportDelta: { + path: 'report/delta/:id/:deltaid', + url: (id: string, deltaId: string) => + `${appendEncodedId('/report/delta', id)}/${encodeURIComponent(deltaId)}`, + }, + certBundAdvisories: { + path: 'cert-bund-advisories', + url: '/cert-bund-advisories', + }, + certBundAdvisory: { + path: 'cert-bund-advisory/:id', + url: (id: string) => appendEncodedId('/cert-bund-advisory', id), + }, + ociImageTargets: {path: 'oci-image-targets', url: '/oci-image-targets'}, + containerImageTarget: { + path: 'oci-image-target/:id', + url: (id: string) => appendEncodedId('/oci-image-target', id), + }, + webApplicationTargets: { + path: 'web-application-targets', + url: '/web-application-targets', + }, + webApplicationTarget: { + path: 'web-application-target/:id', + url: (id: string) => appendEncodedId('/web-application-target', id), + }, + cpes: {path: 'cpes', url: '/cpes'}, + cpe: {path: 'cpe/:id', url: (id: string) => appendEncodedId('/cpe', id)}, + credentials: {path: 'credentials', url: '/credentials'}, + credential: { + path: 'credential/:id', + url: (id: string) => appendEncodedId('/credential', id), + }, + credentialStore: {path: 'credential-store', url: '/credential-store'}, + cves: {path: 'cves', url: '/cves'}, + cve: {path: 'cve/:id', url: (id: string) => appendEncodedId('/cve', id)}, + dfnCertAdvisories: { + path: 'dfn-cert-advisories', + url: '/dfn-cert-advisories', + }, + dfnCertAdvisory: { + path: 'dfn-cert-advisory/:id', + url: (id: string) => appendEncodedId('/dfn-cert-advisory', id), + }, + feedStatus: {path: 'feed-status', url: '/feed-status'}, + filters: {path: 'filters', url: '/filters'}, + filter: { + path: 'filter/:id', + url: (id: string) => appendEncodedId('/filter', id), + }, + groups: {path: 'groups', url: '/groups'}, + group: { + path: 'group/:id', + url: (id: string) => appendEncodedId('/group', id), + }, + hosts: {path: 'hosts', url: '/hosts'}, + host: {path: 'host/:id', url: (id: string) => appendEncodedId('/host', id)}, + ldap: {path: 'ldap', url: '/ldap'}, + notes: {path: 'notes', url: '/notes'}, + note: {path: 'note/:id', url: (id: string) => appendEncodedId('/note', id)}, + nvts: {path: 'nvts', url: '/nvts'}, + nvt: {path: 'nvt/:id', url: (id: string) => appendEncodedId('/nvt', id)}, + operatingSystems: { + path: 'operating-systems', + url: '/operating-systems', + }, + operatingSystem: { + path: 'operating-system/:id', + url: (id: string) => appendEncodedId('/operating-system', id), + }, + overrides: {path: 'overrides', url: '/overrides'}, + override: { + path: 'override/:id', + url: (id: string) => appendEncodedId('/override', id), + }, + performance: {path: 'performance', url: '/performance'}, + permissions: {path: 'permissions', url: '/permissions'}, + permission: { + path: 'permission/:id', + url: (id: string) => appendEncodedId('/permission', id), + }, + policies: {path: 'policies', url: '/policies'}, + policy: { + path: 'policy/:id', + url: (id: string) => appendEncodedId('/policy', id), + }, + portLists: {path: 'port-lists', url: '/port-lists'}, + portList: { + path: 'port-list/:id', + url: (id: string) => appendEncodedId('/port-list', id), + }, + portRange: { + path: 'port-range/:id', + url: (id: string) => appendEncodedId('/port-range', id), + }, + radius: {path: 'radius', url: '/radius'}, + reports: {path: 'reports', url: '/reports'}, + report: { + path: 'report/:id', + url: (id: string) => appendEncodedId('/report', id), + }, + reportConfigs: {path: 'report-configs', url: '/report-configs'}, + reportConfig: { + path: 'report-config/:id', + url: (id: string) => appendEncodedId('/report-config', id), + }, + reportFormats: {path: 'report-formats', url: '/report-formats'}, + reportFormat: { + path: 'report-format/:id', + url: (id: string) => appendEncodedId('/report-format', id), + }, + results: {path: 'results', url: '/results'}, + result: { + path: 'result/:id', + url: (id: string) => appendEncodedId('/result', id), + }, + roles: {path: 'roles', url: '/roles'}, + role: {path: 'role/:id', url: (id: string) => appendEncodedId('/role', id)}, + scanConfigs: {path: 'scan-configs', url: '/scan-configs'}, + scanConfig: { + path: 'scan-config/:id', + url: (id: string) => appendEncodedId('/scan-config', id), + }, + scanners: {path: 'scanners', url: '/scanners'}, + scanner: { + path: 'scanner/:id', + url: (id: string) => appendEncodedId('/scanner', id), + }, + schedules: {path: 'schedules', url: '/schedules'}, + schedule: { + path: 'schedule/:id', + url: (id: string) => appendEncodedId('/schedule', id), + }, + tags: {path: 'tags', url: '/tags'}, + tag: {path: 'tag/:id', url: (id: string) => appendEncodedId('/tag', id)}, + targets: {path: 'targets', url: '/targets'}, + target: { + path: 'target/:id', + url: (id: string) => appendEncodedId('/target', id), + }, + tasks: {path: 'tasks', url: '/tasks'}, + task: {path: 'task/:id', url: (id: string) => appendEncodedId('/task', id)}, + tickets: {path: 'tickets', url: '/tickets'}, + ticket: { + path: 'ticket/:id', + url: (id: string) => appendEncodedId('/ticket', id), + }, + tlsCertificates: { + path: 'tls-certificates', + url: '/tls-certificates', + }, + tlsCertificate: { + path: 'tls-certificate/:id', + url: (id: string) => appendEncodedId('/tls-certificate', id), + }, + trashcan: {path: 'trashcan', url: '/trashcan'}, + users: {path: 'users', url: '/users'}, + user: {path: 'user/:id', url: (id: string) => appendEncodedId('/user', id)}, + userSettings: {path: 'user-settings', url: '/user-settings'}, + vulnerabilities: {path: 'vulnerabilities', url: '/vulnerabilities'}, + vulnerability: { + path: 'vulnerability/:id', + url: (id: string) => appendEncodedId('/vulnerability', id), + }, + asset: { + path: 'asset/:id', + url: (id: string) => appendEncodedId('/asset', id), + }, + info: {path: 'info/:id', url: (id: string) => appendEncodedId('/info', id)}, + cvssCalculator: {path: 'cvss-calculator', url: '/cvss-calculator'}, + legacy: { + auditReports: {path: 'auditreports', url: '/auditreports'}, + auditReport: { + path: 'auditreport/:id', + url: (id: string) => appendEncodedId('/auditreport', id), + }, + certBundAdvisories: {path: 'certbunds', url: '/certbunds'}, + certBundAdvisory: { + path: 'certbund/:id', + url: (id: string) => appendEncodedId('/certbund', id), + }, + ociImageTargets: {path: 'ociimagetargets', url: '/ociimagetargets'}, + webApplicationTargets: { + path: 'webapplicationtargets', + url: '/webapplicationtargets', + }, + credentialStore: {path: 'credentialstore', url: '/credentialstore'}, + dfnCertAdvisories: {path: 'dfncerts', url: '/dfncerts'}, + dfnCertAdvisory: { + path: 'dfncert/:id', + url: (id: string) => appendEncodedId('/dfncert', id), + }, + feedStatus: {path: 'feedstatus', url: '/feedstatus'}, + operatingSystems: {path: 'operatingsystems', url: '/operatingsystems'}, + operatingSystem: { + path: 'operatingsystem/:id', + url: (id: string) => appendEncodedId('/operatingsystem', id), + }, + portLists: {path: 'portlists', url: '/portlists'}, + portList: { + path: 'portlist/:id', + url: (id: string) => appendEncodedId('/portlist', id), + }, + reportConfigs: {path: 'reportconfigs', url: '/reportconfigs'}, + reportConfig: { + path: 'reportconfig/:id', + url: (id: string) => appendEncodedId('/reportconfig', id), + }, + reportFormats: {path: 'reportformats', url: '/reportformats'}, + reportFormat: { + path: 'reportformat/:id', + url: (id: string) => appendEncodedId('/reportformat', id), + }, + scanConfigs: {path: 'scanconfigs', url: '/scanconfigs'}, + scanConfig: { + path: 'scanconfig/:id', + url: (id: string) => appendEncodedId('/scanconfig', id), + }, + tlsCertificates: {path: 'tlscertificates', url: '/tlscertificates'}, + tlsCertificate: { + path: 'tlscertificate/:id', + url: (id: string) => appendEncodedId('/tlscertificate', id), + }, + userSettings: {path: 'usersettings', url: '/usersettings'}, + cvssCalculator: {path: 'cvsscalculator', url: '/cvsscalculator'}, + notFound: {path: 'notfound', url: '/notfound'}, + }, + notFound: {path: 'not-found', url: '/not-found'}, +} as const; + +export type RoutePaths = typeof ROUTES; + +// API entity type names do not always match route keys or URL segments. +const entityRouteNames = { + agent: 'agent', + agentgroup: 'agentGroup', + agentinstaller: 'agentInstaller', + alert: 'alert', + asset: 'asset', + audit: 'audit', + auditreport: 'auditReport', + certbund: 'certBundAdvisory', + cpe: 'cpe', + credential: 'credential', + cve: 'cve', + dfncert: 'dfnCertAdvisory', + filter: 'filter', + group: 'group', + host: 'host', + info: 'info', + ociimagetarget: 'containerImageTarget', + operatingsystem: 'operatingSystem', + webapplicationtarget: 'webApplicationTarget', + override: 'override', + note: 'note', + nvt: 'nvt', + permission: 'permission', + policy: 'policy', + portlist: 'portList', + portrange: 'portRange', + report: 'report', + reportconfig: 'reportConfig', + reportformat: 'reportFormat', + result: 'result', + role: 'role', + scanconfig: 'scanConfig', + scanner: 'scanner', + schedule: 'schedule', + tag: 'tag', + target: 'target', + task: 'task', + ticket: 'ticket', + tlscertificate: 'tlsCertificate', + user: 'user', + vulnerability: 'vulnerability', +} as const satisfies Record; + +export const entityURL = (type: EntityType, id: string): string => { + const route = ROUTES[entityRouteNames[type]]; + return typeof route.url === 'function' ? route.url(id) : route.url; +}; + +const entityListRouteNames = { + agent: 'agents', + agentgroup: 'agentGroups', + agentinstaller: 'agentInstallers', + alert: 'alerts', + audit: 'audits', + auditreport: 'auditReports', + certbund: 'certBundAdvisories', + cpe: 'cpes', + credential: 'credentials', + cve: 'cves', + dfncert: 'dfnCertAdvisories', + filter: 'filters', + group: 'groups', + host: 'hosts', + ociimagetarget: 'ociImageTargets', + operatingsystem: 'operatingSystems', + webapplicationtarget: 'webApplicationTargets', + override: 'overrides', + note: 'notes', + nvt: 'nvts', + permission: 'permissions', + policy: 'policies', + portlist: 'portLists', + report: 'reports', + reportconfig: 'reportConfigs', + reportformat: 'reportFormats', + result: 'results', + role: 'roles', + scanconfig: 'scanConfigs', + scanner: 'scanners', + schedule: 'schedules', + tag: 'tags', + target: 'targets', + task: 'tasks', + ticket: 'tickets', + tlscertificate: 'tlsCertificates', + user: 'users', + vulnerability: 'vulnerabilities', +} as const satisfies Partial>; + +export type EntityListType = keyof typeof entityListRouteNames; + +export const entityListURL = (type: EntityListType): string => + ROUTES[entityListRouteNames[type]].url;