feat: [AH-979]: update view for showing vulnerability scan status in digest list table (#3509)

* feat: [AH-979]: update view for showing vulnerability scan status in digest list table
try-new-ui
Shivanand Sonnad 2025-03-05 14:04:56 +00:00 committed by Harness
parent 4ff47b0c41
commit f5b49c6bab
21 changed files with 86 additions and 104 deletions

View File

@ -51,7 +51,7 @@
"@codemirror/view": "^6.9.6",
"@harnessio/design-system": "^2.1.1",
"@harnessio/icons": "^2.1.9",
"@harnessio/react-har-service-client": "^0.12.0",
"@harnessio/react-har-service-client": "^0.13.0",
"@harnessio/react-ssca-manager-client": "^0.65.0",
"@harnessio/uicore": "^4.1.2",
"@tanstack/react-query": "4.20.4",

View File

@ -56,3 +56,8 @@
--intent-color: #07a0ab;
}
}
.vulnerabilityCellItem {
padding: 0 var(--spacing-small) !important;
border-left: solid var(--spacing-1);
}

View File

@ -25,3 +25,4 @@ export declare const nameCellContainer: string
export declare const nonProd: string
export declare const prod: string
export declare const toggleAccordion: string
export declare const vulnerabilityCellItem: string

View File

@ -259,6 +259,45 @@ export const PullCommandCell = ({ value }: CommonCellProps) => {
return <CommandBlock noWrap commandSnippet={value as string} allowCopy onCopy={killEvent} />
}
interface VulnerabilityCellProps {
critical?: number
high?: number
medium?: number
low?: number
}
export const VulnerabilityCell = ({ critical, high, medium, low }: VulnerabilityCellProps) => {
const { getString } = useStrings()
return (
<Layout.Horizontal>
<Text
className={css.vulnerabilityCellItem}
color={Color.RED_600}
font={{ variation: FontVariation.BODY, weight: 'semi-bold' }}>
{getString('vulnerabilityStatus.critical', { count: critical })}
</Text>
<Text
className={css.vulnerabilityCellItem}
color={Color.ORANGE_900}
font={{ variation: FontVariation.BODY, weight: 'semi-bold' }}>
{getString('vulnerabilityStatus.high', { count: high })}
</Text>
<Text
className={css.vulnerabilityCellItem}
color={Color.YELLOW_900}
font={{ variation: FontVariation.BODY, weight: 'semi-bold' }}>
{getString('vulnerabilityStatus.medium', { count: medium })}
</Text>
<Text
className={css.vulnerabilityCellItem}
color={Color.GREY_500}
font={{ variation: FontVariation.BODY, weight: 'semi-bold' }}>
{getString('vulnerabilityStatus.low', { count: low })}
</Text>
</Layout.Horizontal>
)
}
export default {
UrlCell,
SizeCell,
@ -271,5 +310,6 @@ export default {
PullCommandCell,
LastModifiedCell,
ToggleAccordionCell,
RepositoryLocationBadgeCell
RepositoryLocationBadgeCell,
VulnerabilityCell
}

View File

@ -34,7 +34,7 @@
div[class*='TableV2--cells'],
div[class*='TableV2--header'] {
display: grid !important;
grid-template-columns: minmax(var(--har-table-name-column-min-width), 1fr) 1fr 1fr 1fr 1fr 1fr !important;
grid-template-columns: minmax(var(--har-table-name-column-min-width), 1fr) 1fr 1fr 1fr 1fr minmax(200px, 1fr) !important;
}
div[class*='TableV2--header'] {

View File

@ -24,9 +24,9 @@ import { useStrings } from '@ar/frameworks/strings/String'
import {
DigestActionsCell,
DigestNameCell,
DigestVulnerabilityCell,
DownloadsCell,
OsArchCell,
ScanStatusCell,
SizeCell,
UploadedByCell
} from './DigestTableCells'
@ -73,7 +73,7 @@ export default function DigestListTable(props: DigestListTableProps): JSX.Elemen
{
Header: getString('digestList.table.columns.scanStatus'),
accessor: 'scanStatus',
Cell: ScanStatusCell,
Cell: DigestVulnerabilityCell,
version
},
{

View File

@ -15,11 +15,8 @@
*/
import React from 'react'
import { Link } from 'react-router-dom'
import type { Cell, CellValue, ColumnInstance, Renderer, Row, TableInstance } from 'react-table'
import { Text } from '@harnessio/uicore'
import { Color } from '@harnessio/design-system'
import type { DockerManifestDetails } from '@harnessio/react-har-service-client'
import { useStrings } from '@ar/frameworks/strings'
@ -77,39 +74,20 @@ export const DownloadsCell: CellType = ({ value }) => {
return <TableCells.CountCell value={value} icon="download-box" iconProps={{ size: 12 }} />
}
export const ScanStatusCell: Renderer<{
row: Row<DockerManifestDetails>
column: ColumnInstance<DockerManifestDetails> & DigestNameColumnProps
}> = ({ row, column }) => {
export const DigestVulnerabilityCell: CellType = ({ row }) => {
const { original } = row
const { version } = column
const router = useRoutes()
const { stoExecutionId, stoPipelineId, digest } = original
const pathParams = useDecodedParams<ArtifactDetailsPathParams>()
const { stoDetails } = original
const { getString } = useStrings()
if (!stoExecutionId || !stoPipelineId)
if (!stoDetails)
return <TableCells.TextCell value={getString('artifactList.table.actions.VulnerabilityStatus.nonScanned')} />
const linkTo = router.toARVersionDetailsTab({
repositoryIdentifier: pathParams.repositoryIdentifier,
artifactIdentifier: pathParams.artifactIdentifier,
versionIdentifier: version,
versionTab: VersionDetailsTab.SECURITY_TESTS,
pipelineIdentifier: stoPipelineId,
executionIdentifier: stoExecutionId
})
return (
<Link to={`${linkTo}?digest=${digest}`}>
<Text
color={Color.PRIMARY_7}
rightIcon="main-share"
rightIconProps={{
size: 12
}}
lineClamp={1}>
{getString('artifactList.table.actions.VulnerabilityStatus.scanned')}
</Text>
</Link>
<TableCells.VulnerabilityCell
critical={stoDetails.critical}
high={stoDetails.high}
medium={stoDetails.medium}
low={stoDetails.low}
/>
)
}

View File

@ -36,7 +36,7 @@ interface DockerVersionHeaderProps {
export default function DockerVersionHeader(props: DockerVersionHeaderProps): JSX.Element {
const { iconSize = 40, data } = props
const { imageName: name, version, isLatestVersion = false, packageType } = data
const { imageName: name, version, packageType } = data
const pathParams = useDecodedParams<VersionDetailsPathParams>()
const { useUpdateQueryParams, useQueryParams } = useParentHooks()
const { updateQueryParams } = useUpdateQueryParams()
@ -69,7 +69,7 @@ export default function DockerVersionHeader(props: DockerVersionHeaderProps): JS
digest={digest}
onChangeVersion={handleChangeVersion}
onChangeDigest={handleChangeDigest}
isLatestVersion={isLatestVersion}
isLatestVersion={false}
/>
<Expander />
<SetupClientButton

View File

@ -34,7 +34,6 @@ export const mockDockerLatestVersionListTableData: ListArtifactVersion = {
prodEnvCount: 0
},
digestCount: 1,
islatestVersion: true,
lastModified: '1730978736333',
name: '1.0.0',
packageType: 'DOCKER',
@ -108,7 +107,6 @@ export const mockDockerVersionList: ListArtifactVersionResponseResponse = {
},
digestCount: 1,
downloadsCount: 11,
islatestVersion: false,
lastModified: '1738923119434',
name: '1.0.0',
packageType: 'DOCKER',
@ -124,7 +122,6 @@ export const mockDockerVersionList: ListArtifactVersionResponseResponse = {
},
digestCount: 1,
downloadsCount: 11,
islatestVersion: false,
lastModified: '1738923402541',
name: '1.0.1',
packageType: 'DOCKER',
@ -140,7 +137,6 @@ export const mockDockerVersionList: ListArtifactVersionResponseResponse = {
},
digestCount: 1,
downloadsCount: 11,
islatestVersion: true,
lastModified: '1738924148637',
name: '1.0.2',
packageType: 'DOCKER',
@ -191,7 +187,6 @@ export const mockDockerArtifactDetails: DockerArtifactDetailResponseResponse = {
createdAt: '1738923119434',
downloadsCount: 11,
imageName: 'maven-app',
isLatestVersion: false,
modifiedAt: '1738923119434',
packageType: 'DOCKER',
pullCommand: 'docker pull pkg.qa.harness.io/iwnhltqot7gft7r-f_zp7q/docker-repo/maven-app:1.0.0',

View File

@ -60,7 +60,6 @@ export const mockGenericVersionList: ListArtifactVersionResponseResponse = {
},
digestCount: 1,
downloadsCount: 11,
islatestVersion: false,
lastModified: '1738923119434',
name: '1.0.0',
packageType: 'GENERIC',
@ -76,7 +75,6 @@ export const mockGenericVersionList: ListArtifactVersionResponseResponse = {
},
digestCount: 1,
downloadsCount: 11,
islatestVersion: false,
lastModified: '1738923402541',
name: '1.0.1',
packageType: 'GENERIC',
@ -92,7 +90,6 @@ export const mockGenericVersionList: ListArtifactVersionResponseResponse = {
},
digestCount: 1,
downloadsCount: 11,
islatestVersion: true,
lastModified: '1738924148637',
name: '1.0.2',
packageType: 'GENERIC',

View File

@ -27,7 +27,6 @@ export const mockHelmLatestVersionListTableData: ListArtifactVersion = {
{
digestCount: 1,
downloadsCount: 100,
islatestVersion: true,
lastModified: '1730978736333',
name: '1.0.0',
packageType: 'HELM',
@ -46,7 +45,6 @@ export const mockHelmLatestVersionListTableData: ListArtifactVersion = {
export const mockHelmVersionSummary: ArtifactVersionSummaryResponseResponse = {
data: {
imageName: 'maven-app',
isLatestVersion: true,
packageType: 'HELM',
version: '1.0.0'
},
@ -63,7 +61,6 @@ export const mockHelmVersionList: ListArtifactVersionResponseResponse = {
},
digestCount: 1,
downloadsCount: 11,
islatestVersion: false,
lastModified: '1738923119434',
name: '1.0.0',
packageType: 'HELM',
@ -79,7 +76,6 @@ export const mockHelmVersionList: ListArtifactVersionResponseResponse = {
},
digestCount: 1,
downloadsCount: 11,
islatestVersion: false,
lastModified: '1738923402541',
name: '1.0.1',
packageType: 'HELM',
@ -95,7 +91,6 @@ export const mockHelmVersionList: ListArtifactVersionResponseResponse = {
},
digestCount: 1,
downloadsCount: 11,
islatestVersion: true,
lastModified: '1738924148637',
name: '1.0.2',
packageType: 'HELM',
@ -118,7 +113,6 @@ export const mockHelmArtifactDetails: HelmArtifactDetailResponseResponse = {
artifact: 'production',
createdAt: '1729862063842',
downloadsCount: 0,
isLatestVersion: true,
modifiedAt: '1729862063842',
packageType: 'HELM',
pullCommand: 'helm pull oci://pkg.qa.harness.io/iwnhltqot7gft7r-f_zp7q/helm-repo-1/production:1.0.15',

View File

@ -43,7 +43,6 @@ export const mockMavenLatestVersionListTableData: ListArtifactVersion = {
export const mockMavenVersionSummary: ArtifactVersionSummaryResponseResponse = {
data: {
imageName: 'maven-app',
isLatestVersion: false,
packageType: 'MAVEN',
version: '1.0.0'
},
@ -60,7 +59,6 @@ export const mockMavenVersionList: ListArtifactVersionResponseResponse = {
},
digestCount: 1,
downloadsCount: 11,
islatestVersion: false,
lastModified: '1738923119434',
name: '1.0.0',
packageType: 'MAVEN',
@ -76,7 +74,6 @@ export const mockMavenVersionList: ListArtifactVersionResponseResponse = {
},
digestCount: 1,
downloadsCount: 11,
islatestVersion: false,
lastModified: '1738923402541',
name: '1.0.1',
packageType: 'MAVEN',
@ -92,7 +89,6 @@ export const mockMavenVersionList: ListArtifactVersionResponseResponse = {
},
digestCount: 1,
downloadsCount: 11,
islatestVersion: true,
lastModified: '1738924148637',
name: '1.0.2',
packageType: 'MAVEN',

View File

@ -42,6 +42,10 @@ export default function OSSVersionDetailsPage() {
}
const [showModal, hideModal] = useModalHook(() => {
const handleCloseDetailsModal = () => {
hideModal()
handleCloseModal()
}
return (
<Drawer
className="arApp"
@ -49,16 +53,13 @@ export default function OSSVersionDetailsPage() {
isOpen={true}
isCloseButtonShown={false}
size={'70%'}
onClose={() => {
hideModal()
handleCloseModal()
}}>
onClose={handleCloseDetailsModal}>
<VersionProvider
repoKey={pathParams.repositoryIdentifier}
artifactKey={pathParams.artifactIdentifier}
versionKey={pathParams.versionIdentifier}>
<Container data-testid="version-details-page-oss" className={css.ossVersionDetailsModal}>
<Button minimal className={css.closeBtn} icon="cross" withoutBoxShadow onClick={hideModal} />
<Button minimal className={css.closeBtn} icon="cross" withoutBoxShadow onClick={handleCloseDetailsModal} />
<OSSVersionDetails />
</Container>
</VersionProvider>

View File

@ -35,7 +35,7 @@ interface VersionDetailsHeaderContentProps {
export default function VersionDetailsHeaderContent(props: VersionDetailsHeaderContentProps): JSX.Element {
const { iconSize = 40, data } = props
const { imageName, version, isLatestVersion = false, packageType } = data
const { imageName, version, packageType } = data
const pathParams = useDecodedParams<VersionDetailsPathParams>()
const history = useHistory()
const routes = useRoutes()
@ -57,7 +57,7 @@ export default function VersionDetailsHeaderContent(props: VersionDetailsHeaderC
name={imageName}
version={version}
onChangeVersion={handleChangeVersion}
isLatestVersion={isLatestVersion}
isLatestVersion={false}
/>
<Expander />
<SetupClientButton

View File

@ -20,8 +20,6 @@ import { Layout } from '@harnessio/uicore'
import { Icon } from '@harnessio/icons'
import type { ArtifactVersionMetadata } from '@harnessio/react-har-service-client'
import Tag from '@ar/components/Tag/Tag'
import { useStrings } from '@ar/frameworks/strings'
import TableCells from '@ar/components/TableCells/TableCells'
import css from './DockerVersionListTable.module.scss'
@ -35,14 +33,11 @@ type CellTypeWithActions<D extends Record<string, any>, V = any> = TableInstance
type CellType = Renderer<CellTypeWithActions<ArtifactVersionMetadata>>
export const DockerVersionNameCell: CellType = ({ value, row }) => {
const { original } = row
const { getString } = useStrings()
export const DockerVersionNameCell: CellType = ({ value }) => {
return (
<Layout.Horizontal className={css.nameCellContainer} spacing="small">
<Icon name="store-artifact-bundle" size={24} />
<TableCells.TextCell value={value} />
{original.islatestVersion && <Tag isVersionTag>{getString('tags.latest')}</Tag>}
</Layout.Horizontal>
)
}

View File

@ -24,7 +24,6 @@ export const mockHelmLatestVersionListTableData: ListArtifactVersion = {
prodEnvCount: 0
},
digestCount: 1,
islatestVersion: true,
lastModified: '1729861854693',
name: '1.0.15',
packageType: 'HELM',
@ -49,7 +48,6 @@ export const mockHelmNoPullCmdVersionListTableData: ListArtifactVersion = {
prodEnvCount: 0
},
digestCount: 1,
islatestVersion: true,
lastModified: '1729861854693',
name: '1.0.15',
packageType: 'HELM',
@ -74,7 +72,6 @@ export const mockHelmOldVersionListTableData: ListArtifactVersion = {
prodEnvCount: 0
},
digestCount: 1,
islatestVersion: false,
lastModified: '1729861854693',
name: '1.0.15',
packageType: 'HELM',
@ -99,7 +96,6 @@ export const mockDockerNoPullCmdVersionListTableData: ListArtifactVersion = {
prodEnvCount: 0
},
digestCount: 1,
islatestVersion: true,
lastModified: '1730978736333',
name: '1.0.0',
packageType: 'DOCKER',
@ -123,7 +119,6 @@ export const mockDockerLatestVersionListTableData: ListArtifactVersion = {
prodEnvCount: 0
},
digestCount: 1,
islatestVersion: true,
lastModified: '1730978736333',
name: '1.0.0',
packageType: 'DOCKER',
@ -147,7 +142,6 @@ export const mockDockerOldVersionListTableData: ListArtifactVersion = {
prodEnvCount: 0
},
digestCount: 1,
islatestVersion: false,
lastModified: '1730978736333',
name: '1.0.0',
packageType: 'DOCKER',

View File

@ -23,7 +23,6 @@ import { Icon } from '@harnessio/icons'
import { Layout, Text } from '@harnessio/uicore'
import type { ArtifactVersionMetadata } from '@harnessio/react-har-service-client'
import Tag from '@ar/components/Tag/Tag'
import { useStrings } from '@ar/frameworks/strings'
import { useDecodedParams, useRoutes } from '@ar/hooks'
import TableCells from '@ar/components/TableCells/TableCells'
@ -60,9 +59,7 @@ export const ToggleAccordionCell: Renderer<{
)
}
export const VersionNameCell: CellType = ({ value, row }) => {
const { original } = row
const { getString } = useStrings()
export const VersionNameCell: CellType = ({ value }) => {
const routes = useRoutes()
const pathParams = useDecodedParams<ArtifactDetailsPathParams>()
return (
@ -79,7 +76,6 @@ export const VersionNameCell: CellType = ({ value, row }) => {
{value}
</Text>
</Link>
{original.islatestVersion && <Tag isVersionTag>{getString('tags.latest')}</Tag>}
</Layout.Horizontal>
)
}

View File

@ -15,7 +15,7 @@
*/
import React from 'react'
import { getByText, queryByText, render } from '@testing-library/react'
import { getByText, render } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import copy from 'clipboard-copy'
import repositoryFactory from '@ar/frameworks/RepositoryStep/RepositoryFactory'
@ -67,8 +67,6 @@ describe('Verify Version List Table', () => {
expect(rows).toHaveLength(1)
const getFirstRowColumn = (col: number) => getTableColumn(1, col) as HTMLElement
const latestTag = getByText(getFirstRowColumn(1), 'tags.latest')
expect(latestTag).toBeInTheDocument()
const sizeValue = getByText(
getFirstRowColumn(2),
mockHelmLatestVersionListTableData.artifactVersions?.[0].size as string
@ -108,23 +106,6 @@ describe('Verify Version List Table', () => {
expect(copyCurlBtn).not.toBeInTheDocument()
})
test('Should not show latest tag if item version is not latest', () => {
render(
<ArTestWrapper>
<VersionListTable
data={mockHelmOldVersionListTableData}
gotoPage={jest.fn()}
setSortBy={jest.fn()}
sortBy={['name', 'DESC']}
columnConfigs={mockColumnConfigs}
/>
</ArTestWrapper>
)
const getFirstTableColumn = (col: number) => getTableColumn(col, 1) as HTMLElement
const latestTag = queryByText(getFirstTableColumn(1), 'tags.latest')
expect(latestTag).not.toBeInTheDocument()
})
test('Should show no rows if no data is provided', () => {
const { container } = render(
<ArTestWrapper>

View File

@ -109,3 +109,8 @@ validationMessages:
passwordRequired: Password is required
accessKeyRequired: Access key is required
secretKeyRequired: Secret key is required
vulnerabilityStatus:
critical: 'C {{count}}'
high: 'H {{count}}'
medium: 'M {{count}}'
low: 'L {{count}}'

View File

@ -392,4 +392,8 @@ export interface StringsMap {
'validationMessages.urlRequired': string
'validationMessages.userNameRequired': string
view: string
'vulnerabilityStatus.critical': string
'vulnerabilityStatus.high': string
'vulnerabilityStatus.low': string
'vulnerabilityStatus.medium': string
}

View File

@ -1945,10 +1945,10 @@
yargs "^17.6.2"
zod "^3.19.1"
"@harnessio/react-har-service-client@^0.12.0":
version "0.12.0"
resolved "https://registry.yarnpkg.com/@harnessio/react-har-service-client/-/react-har-service-client-0.12.0.tgz#9ad2dec1a4ba2d4150e9c4dea67e2f867d9141ed"
integrity sha512-bBDrVL/OkX14SdG3XS5/h7W5pQ0BH6tXhK1w2F9eWf5qupyZe2lwaDpVRzxi2fkzr0wojf2wd/e3p3IiB47X0g==
"@harnessio/react-har-service-client@^0.13.0":
version "0.13.0"
resolved "https://registry.yarnpkg.com/@harnessio/react-har-service-client/-/react-har-service-client-0.13.0.tgz#c71ff5f4684b6bc5aaaa8e3b28b379fc50a9a71f"
integrity sha512-b0gehNm752mE4FqbdYUJlv+tceqetsw4I4P2qMW9vjA1NLIz0Bwo1qpF1CDh8iJmbAwdkYjaiVftuefzVNQh4w==
dependencies:
"@harnessio/oats-cli" "^3.0.0"