diff --git a/ui/apps/platform/cypress/integration-ocp/security/cveDetail.test.ts b/ui/apps/platform/cypress/integration-ocp/security/cveDetail.test.ts index 54880cfbc1039..1eba74dab6eca 100644 --- a/ui/apps/platform/cypress/integration-ocp/security/cveDetail.test.ts +++ b/ui/apps/platform/cypress/integration-ocp/security/cveDetail.test.ts @@ -7,53 +7,71 @@ import { selectors } from '../../integration/vulnerabilities/workloadCves/Worklo import { selectors as vulnerabilitiesSelectors } from '../../integration/vulnerabilities/vulnerabilities.selectors'; import pf6 from '../../selectors/pf6'; +function visitFirstCve() { + withOcpAuth(); + visitFromConsoleLeftNavExpandable('Security', 'Vulnerabilities'); + + return cy + .get(`${selectors.firstTableRow} td[data-label="CVE"]`) + .click() + .invoke('text') + .then((cveName) => { + cy.get('h1').contains(new RegExp(`^${cveName}$`)); + return Promise.resolve(cveName); + }); +} + describe('Security vulnerabilities - CVE Detail page', () => { it('should navigate to the CVE Detail page and account for the project filter', () => { - withOcpAuth(); - visitFromConsoleLeftNavExpandable('Security', 'Vulnerabilities'); + visitFirstCve().then(() => { + // Verify that "All projects" is selected + cy.get(`.co-namespace-bar ${pf6.menuToggle}`).contains('All Projects'); - // Visit a CVE page via link in the CVE table - cy.get(`${selectors.firstTableRow} td[data-label="CVE"]`) - .click() - .invoke('text') - .then((cveName) => { - cy.get('h1').contains(new RegExp(`^${cveName}$`)); + // Click the deployment entity toggle + cy.get(vulnerabilitiesSelectors.entityTypeToggleItem('Deployment')).click(); - // Verify that "All projects" is selected - cy.get(`.co-namespace-bar ${pf6.menuToggle}`).contains('All Projects'); + // Columns that are always present in the table + const baseColumns = [ + 'Row expansion', + 'Deployment', + 'Images by severity', + 'Images', + 'First discovered', + ]; - // Click the deployment entity toggle - cy.get(vulnerabilitiesSelectors.entityTypeToggleItem('Deployment')).click(); + const topLevelTableSelector = 'table:first-of-type'; - // Columns that are always present in the table - const baseColumns = [ - 'Row expansion', - 'Deployment', - 'Images by severity', - 'Images', - 'First discovered', - ]; + // Verify that the "Namespace" column is present + assertVisibleTableColumns(topLevelTableSelector, [...baseColumns, 'Namespace']); - const topLevelTableSelector = 'table:first-of-type'; + // Verify that Namespace is present in the search entities + assertSearchEntities(['Image', 'Image component', 'Deployment', 'Namespace']); - // Verify that the "Namespace" column is present - assertVisibleTableColumns(topLevelTableSelector, [...baseColumns, 'Namespace']); + // Change to the 'stackrox' project + selectProject('stackrox'); - // Verify that Namespace is present in the search entities - assertSearchEntities(['Image', 'Image component', 'Deployment', 'Namespace']); + // Wait for the table data to update + cy.get(selectors.loadingSpinner).should('exist'); + cy.get(selectors.loadingSpinner).should('not.exist'); - // Change to the 'stackrox' project - selectProject('stackrox'); + // Verify that the "Namespace" column is not present + assertVisibleTableColumns(topLevelTableSelector, [...baseColumns]); - // Wait for the table data to update - cy.get(selectors.loadingSpinner).should('exist'); - cy.get(selectors.loadingSpinner).should('not.exist'); + // Verify that Namespace is not present in the search entities + assertSearchEntities(['Image', 'Image component', 'Deployment']); + }); + }); - // Verify that the "Namespace" column is not present - assertVisibleTableColumns(topLevelTableSelector, [...baseColumns]); + it('should navigate to an affected image detail page', () => { + visitFirstCve().then(() => { + cy.get(vulnerabilitiesSelectors.entityTypeToggleItem('Image')).click(); - // Verify that Namespace is not present in the search entities - assertSearchEntities(['Image', 'Image component', 'Deployment']); - }); + cy.get(`${selectors.firstTableRow} td[data-label="Image"] a`) + .click() + .then(([$imageLink]) => { + const imageName = $imageLink.innerText.replace('\n', ''); + cy.get('h1').contains(imageName); + }); + }); }); }); diff --git a/ui/apps/platform/cypress/integration-ocp/security/imageDetail.test.ts b/ui/apps/platform/cypress/integration-ocp/security/imageDetail.test.ts new file mode 100644 index 0000000000000..9ef42e5f2dfc0 --- /dev/null +++ b/ui/apps/platform/cypress/integration-ocp/security/imageDetail.test.ts @@ -0,0 +1,62 @@ +import { visitFromConsoleLeftNavExpandable } from '../../helpers/nav'; +import { withOcpAuth } from '../../helpers/ocpAuth'; +import { assertVisibleTableColumns } from '../../helpers/tableHelpers'; +import { selectors } from '../../integration/vulnerabilities/workloadCves/WorkloadCves.selectors'; +import { selectors as vulnerabilitiesSelectors } from '../../integration/vulnerabilities/vulnerabilities.selectors'; +import { selectProject } from '../../helpers/ocpConsole'; + +function visitImageDetailPage() { + withOcpAuth(); + visitFromConsoleLeftNavExpandable('Security', 'Vulnerabilities'); + + cy.get(vulnerabilitiesSelectors.entityTypeToggleItem('Image')).click(); + + // Visit an image page via link in the image table + return cy + .get(`${selectors.firstTableRow} td[data-label="Image"] a`) + .click() + .then(([$imageLink]) => { + const imageName = $imageLink.innerText.replace('\n', ''); + cy.get('h1').contains(imageName); + return Promise.resolve(imageName); + }); +} + +describe('Security vulnerabilities - Image Detail page', () => { + it('should show the appropriate table columns on the workload resources tab', () => { + visitImageDetailPage() + .then(() => { + cy.get('button[role="tab"]:contains("Resources")').click(); + + // By default, the project filter should be "All projects" which will show the Namespace column + const expectedColumns = ['Name', 'Namespace', 'Created']; + assertVisibleTableColumns('table', expectedColumns); + + // The user could also navigate to this page when viewing a project that has a workload containing the image. + // Grab the namespace of a known workload so we can select that project. + return cy + .get(`${selectors.firstTableRow} td[data-label="Namespace"]`) + .then(([$ns]) => Promise.resolve($ns.innerText)); + }) + .then((namespace) => { + // Select the project that has the workload containing the image and verify the columns + selectProject(namespace); + + const expectedColumns = ['Name', 'Created']; + assertVisibleTableColumns('table', expectedColumns); + }); + }); + + it('should navigate to the CVE Detail from the vulnerability table for the image', () => { + visitImageDetailPage() + .then(() => { + return cy + .get(`${selectors.firstTableRow} td[data-label="CVE"] a`) + .click() + .then(([$cveLink]) => Promise.resolve($cveLink.innerText.replace('\n', ''))); + }) + .then((cveName) => { + cy.get('h1').contains(cveName); + }); + }); +}); diff --git a/ui/apps/platform/src/ConsolePlugin/ImageDetailPage/ImageDetailPage.tsx b/ui/apps/platform/src/ConsolePlugin/ImageDetailPage/ImageDetailPage.tsx index 9b1d2bf026d3d..f6562387c8a76 100644 --- a/ui/apps/platform/src/ConsolePlugin/ImageDetailPage/ImageDetailPage.tsx +++ b/ui/apps/platform/src/ConsolePlugin/ImageDetailPage/ImageDetailPage.tsx @@ -1,16 +1,32 @@ import React from 'react'; -import { useActiveNamespace } from '@openshift-console/dynamic-plugin-sdk'; -import { useParams } from 'react-router-dom-v5-compat'; +import { NamespaceBar, useActiveNamespace } from '@openshift-console/dynamic-plugin-sdk'; + +import { ALL_NAMESPACES_KEY } from 'ConsolePlugin/constants'; +import useURLSearch from 'hooks/useURLSearch'; +import ImagePage from 'Containers/Vulnerabilities/WorkloadCves/Image/ImagePage'; +import { useDefaultWorkloadCveViewContext } from 'ConsolePlugin/hooks/useDefaultWorkloadCveViewContext'; +import { WorkloadCveViewContext } from 'Containers/Vulnerabilities/WorkloadCves/WorkloadCveViewContext'; +import { hideColumnIf } from 'hooks/useManagedColumns'; export function ImageDetailPage() { - const [namespace] = useActiveNamespace(); - const { imageId } = useParams(); + const { searchFilter, setSearchFilter } = useURLSearch(); + const context = useDefaultWorkloadCveViewContext(); + const [activeNamespace] = useActiveNamespace(); return ( - <> -
Image Detail Page
-
Namespace: {namespace}
-
Image ID: {imageId}
- + + setSearchFilter({ ...searchFilter, Namespace: [] })} + /> + + ); } diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/DeploymentResourceTable.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/DeploymentResourceTable.tsx index bc5d257487573..3dc676cdcae65 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/DeploymentResourceTable.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/DeploymentResourceTable.tsx @@ -4,11 +4,34 @@ import { Table, Thead, Tr, Th, Tbody, Td } from '@patternfly/react-table'; import { gql } from '@apollo/client'; import { UseURLSortResult } from 'hooks/useURLSort'; +import { generateVisibilityForColumns, ManagedColumns } from 'hooks/useManagedColumns'; import DateDistance from 'Components/DateDistance'; import EmptyTableResults from '../components/EmptyTableResults'; import useVulnerabilityState from '../hooks/useVulnerabilityState'; import useWorkloadCveViewContext from '../hooks/useWorkloadCveViewContext'; +export const deploymentResourcesTableId = 'DeploymentResourcesTable'; + +export const defaultColumns = { + name: { + title: 'Name', + isShownByDefault: true, + isUntoggleAble: true, + }, + cluster: { + title: 'Cluster', + isShownByDefault: true, + }, + namespace: { + title: 'Namespace', + isShownByDefault: true, + }, + created: { + title: 'Created', + isShownByDefault: true, + }, +} as const; + export type DeploymentResources = { deploymentCount: number; deployments: { @@ -38,19 +61,34 @@ export const deploymentResourcesFragment = gql` export type DeploymentResourceTableProps = { data: DeploymentResources; getSortParams: UseURLSortResult['getSortParams']; + columnVisibilityState: ManagedColumns['columns']; }; -function DeploymentResourceTable({ data, getSortParams }: DeploymentResourceTableProps) { +function DeploymentResourceTable({ + data, + getSortParams, + columnVisibilityState, +}: DeploymentResourceTableProps) { const { urlBuilder } = useWorkloadCveViewContext(); const vulnerabilityState = useVulnerabilityState(); + const getVisibilityClass = generateVisibilityForColumns(columnVisibilityState); return ( - - - - + + + + {data.deployments.length === 0 && } @@ -63,7 +101,7 @@ function DeploymentResourceTable({ data, getSortParams }: DeploymentResourceTabl }} > - - - - + + diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePage.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePage.tsx index 1f4004837ade8..f4a5e8b62ff84 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePage.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePage.tsx @@ -30,6 +30,7 @@ import { getAxiosErrorMessage } from 'utils/responseErrorUtils'; import useIsScannerV4Enabled from 'hooks/useIsScannerV4Enabled'; import usePermissions from 'hooks/usePermissions'; import useURLPagination from 'hooks/useURLPagination'; +import type { ColumnConfigOverrides } from 'hooks/useManagedColumns'; import type { VulnerabilityState } from 'types/cve.proto'; import HeaderLoadingSkeleton from '../../components/HeaderLoadingSkeleton'; @@ -49,6 +50,7 @@ import getImageScanMessage from '../utils/getImageScanMessage'; import { DEFAULT_VM_PAGE_SIZE } from '../../constants'; import { getImageBaseNameDisplay } from '../utils/images'; import useWorkloadCveViewContext from '../hooks/useWorkloadCveViewContext'; +import { defaultColumns as deploymentResourcesDefaultColumns } from './DeploymentResourceTable'; export const imageDetailsQuery = gql` ${imageDetailsFragment} @@ -82,9 +84,16 @@ function OptionalSbomButtonTooltip({ export type ImagePageProps = { vulnerabilityState: VulnerabilityState; showVulnerabilityStateTabs: boolean; + deploymentResourceColumnOverrides: ColumnConfigOverrides< + keyof typeof deploymentResourcesDefaultColumns + >; }; -function ImagePage({ vulnerabilityState, showVulnerabilityStateTabs }: ImagePageProps) { +function ImagePage({ + vulnerabilityState, + showVulnerabilityStateTabs, + deploymentResourceColumnOverrides, +}: ImagePageProps) { const { urlBuilder, pageTitle } = useWorkloadCveViewContext(); const { imageId } = useParams() as { imageId: string }; const { data, error } = useQuery< @@ -262,7 +271,13 @@ function ImagePage({ vulnerabilityState, showVulnerabilityStateTabs }: ImagePage eventKey="Resources" title={Resources} > - + ; }; const imageResourcesQuery = gql` @@ -38,7 +45,11 @@ const imageResourcesQuery = gql` } `; -function ImagePageResources({ imageId, pagination }: ImagePageResourcesProps) { +function ImagePageResources({ + imageId, + pagination, + deploymentResourceColumnOverrides, +}: ImagePageResourcesProps) { const { baseSearchFilter } = useWorkloadCveViewContext(); const { page, perPage, setPage, setPerPage } = pagination; const { sortOption, getSortParams } = useURLSort({ @@ -63,6 +74,16 @@ function ImagePageResources({ imageId, pagination }: ImagePageResourcesProps) { const imageResourcesData = data?.image ?? previousData?.image; const deploymentCount = imageResourcesData?.deploymentCount ?? 0; + const deploymentResourceColumnState = useManagedColumns( + deploymentResourcesTableId, + deploymentResourcesDefaultColumns + ); + + const deploymentResourceColumnConfig = overrideManagedColumns( + deploymentResourceColumnState.columns, + deploymentResourceColumnOverrides + ); + return ( <> @@ -111,6 +132,7 @@ function ImagePageResources({ imageId, pagination }: ImagePageResourcesProps) { diff --git a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePageRoute.tsx b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePageRoute.tsx index fb54586ab7c2c..f1f1920350cfd 100644 --- a/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePageRoute.tsx +++ b/ui/apps/platform/src/Containers/Vulnerabilities/WorkloadCves/Image/ImagePageRoute.tsx @@ -5,7 +5,13 @@ import useVulnerabilityState from '../hooks/useVulnerabilityState'; function ImagePageRoute() { const vulnerabilityState = useVulnerabilityState(); - return ; + return ( + + ); } export default ImagePageRoute;
NameClusterNamespaceCreated + Name + + Cluster + + Namespace + Created
+ {clusterName}{namespace} + + {clusterName} + + {namespace} +