feat: [AH-679]: support new design for version details page (#3015)

* feat: [AH-679]: fix spelling mistake
* feat: [AH-679]: support new design for version details page
pull/3590/head
Shivanand Sonnad 2024-11-19 12:49:10 +00:00 committed by Harness
parent 9ce4ea8ee5
commit 37c7d3c626
25 changed files with 280 additions and 181 deletions

View File

@ -14,10 +14,8 @@
* limitations under the License.
*/
import React, { PropsWithChildren, Suspense, useEffect, useMemo, useRef } from 'react'
import React, { PropsWithChildren, Suspense, useEffect } from 'react'
import { Page } from '@harnessio/uicore'
import { HARServiceAPIClient } from '@harnessio/react-har-service-client'
import { SSCAManagerAPIClient } from '@harnessio/react-ssca-manager-client'
import { QueryClientProvider } from '@tanstack/react-query'
import { StringsContextProvider } from '@ar/frameworks/strings/StringsContextProvider'
@ -32,6 +30,8 @@ import type { MFEAppProps } from '@ar/MFEAppTypes'
import DefaultNavComponent from '@ar/__mocks__/components/DefaultNavComponent'
import AppErrorBoundary from '@ar/components/AppErrorBoundary/AppErrorBoundary'
import useOpenApiClient from './useOpenApiClient'
// Start: Add all factory registractions here
import '@ar/pages/version-details/VersionFactory'
import '@ar/pages/repository-details/RepositoryFactory'
@ -60,33 +60,7 @@ export default function ChildApp(props: PropsWithChildren<MFEAppProps>): React.R
const { ModalProvider } = customComponents
const appStoreData = React.useContext(parentContextObj.appStoreContext)
const apiClientOptions = useMemo(
() => ({
responseInterceptor: (response: Response): Response => {
if (!response.ok && response.status === 401) {
on401()
}
return response
},
urlInterceptor: (url: string) => {
return customUtils.getApiBaseUrl(url)
},
requestInterceptor(request: Request) {
request.headers.delete('Authorization')
// add custom headers if available
const customHeader = customUtils.getCustomHeaders()
Object.entries(customHeader).map(([key, value]) => {
request.headers.set(key, value)
})
return request
}
}),
[]
)
useRef<HARServiceAPIClient>(new HARServiceAPIClient(apiClientOptions))
useRef<SSCAManagerAPIClient>(new SSCAManagerAPIClient(apiClientOptions))
useOpenApiClient({ on401, customUtils })
useEffect(
() => () => {

View File

@ -0,0 +1,65 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useRef } from 'react'
import { HARServiceAPIClient } from '@harnessio/react-har-service-client'
import { SSCAManagerAPIClient } from '@harnessio/react-ssca-manager-client'
import type { CustomUtils } from '@ar/MFEAppTypes'
interface useOpenApiClientProps {
on401: () => void
customUtils: CustomUtils
}
export default function useOpenApiClient({ on401, customUtils }: useOpenApiClientProps) {
const responseInterceptor = (response: Response): Response => {
if (!response.ok && response.status === 401) {
on401()
}
return response
}
const requestInterceptor = (request: Request) => {
request.headers.delete('Authorization')
// add custom headers if available
const customHeader = customUtils.getCustomHeaders()
Object.entries(customHeader).map(([key, value]) => {
request.headers.set(key, value)
})
return request
}
useRef<HARServiceAPIClient>(
new HARServiceAPIClient({
responseInterceptor,
requestInterceptor,
urlInterceptor: (url: string) => {
return customUtils.getApiBaseUrl(url)
}
})
)
useRef<SSCAManagerAPIClient>(
new SSCAManagerAPIClient({
responseInterceptor,
requestInterceptor,
urlInterceptor: (url: string) => {
return window.getApiBaseUrl(`/ssca-manager${url}`)
}
})
)
}

View File

@ -91,7 +91,7 @@ export default function DonutChart(props: DonutChartProps) {
const parsedOptions = useMemo(() => getParsedOptions(defaultOptions, options), [defaultOptions, options])
return (
<Layout.Horizontal spacing="medium" flex={{ alignItems: 'center' }}>
<Layout.Horizontal spacing="small" flex={{ alignItems: 'center' }}>
<HighchartsReact highcharts={Highcharts} options={parsedOptions}></HighchartsReact>
<LegendList items={items} />
</Layout.Horizontal>

View File

@ -15,20 +15,11 @@
*/
import React from 'react'
import { Layout, Utils, Text } from '@harnessio/uicore'
import { FontVariation, type Color } from '@harnessio/design-system'
import { Layout } from '@harnessio/uicore'
import { DonutChartTag } from '@ar/components/Tag/Tags'
import type { PieChartItem } from '../types'
import css from './DonutChart.module.scss'
export const Legend = ({ label, color }: { label: React.ReactNode; color?: Color }) => {
return (
<Layout.Horizontal spacing="small" flex={{ alignItems: 'center' }}>
<div className={css.legendColor} style={{ background: Utils.getRealCSSColor(color || '') }}></div>
{label}
</Layout.Horizontal>
)
}
export const LegendList = ({ items }: { items: PieChartItem[] }) => {
return (
@ -37,8 +28,9 @@ export const LegendList = ({ items }: { items: PieChartItem[] }) => {
{items.map(item => {
return (
<Layout.Horizontal spacing={'xsmall'} key={item.value}>
<Legend label={<Text font={{ variation: FontVariation.SMALL }}>{item.label}</Text>} color={item.color} />
<Text font={{ variation: FontVariation.SMALL }}>({item.value})</Text>
<DonutChartTag
color={item.color}
backgroundColor={item.backgroundColor}>{`${item.label} ${item.value}`}</DonutChartTag>
</Layout.Horizontal>
)
})}

View File

@ -21,4 +21,5 @@ export interface PieChartItem {
value: number
formattedValue?: string | React.ReactElement
color?: Color
backgroundColor?: Color
}

View File

@ -45,5 +45,5 @@
.nonProdTag {
background-color: var(--teal-50) !important;
color: var(--team-900) !important;
color: var(--teal-900) !important;
}

View File

@ -14,9 +14,10 @@
* limitations under the License.
*/
import React from 'react'
import React, { PropsWithChildren } from 'react'
import classNames from 'classnames'
import { Tag } from '@blueprintjs/core'
import { Utils } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings'
@ -39,3 +40,22 @@ export function NonProdTag() {
</Tag>
)
}
interface DonutChartTagProps {
color?: string
backgroundColor?: string
}
export function DonutChartTag(props: PropsWithChildren<DonutChartTagProps>) {
return (
<Tag
className={classNames(css.tag)}
style={{
backgroundColor: Utils.getRealCSSColor(props.backgroundColor as string),
color: Utils.getRealCSSColor(props.color as string)
}}
round>
{props.children}
</Tag>
)
}

View File

@ -14,24 +14,22 @@
* limitations under the License.
*/
.securityTestsCard {
flex: 1;
}
.container {
height: auto;
min-height: 200px;
min-height: 160px;
width: 100%;
}
.cardsContainer {
display: grid;
grid-template-columns: auto max-content max-content;
column-gap: var(--spacing-small);
}
.card {
cursor: pointer;
&:hover {
border: 1px solid var(--primary-2);
box-shadow: 0px 4px 8px 0px rgba(69, 71, 101, 0.16), 0px 0px 2px 0px rgba(40, 41, 61, 0.04);
}
&.deploymentsCard {
width: 40%;
}
}

View File

@ -17,6 +17,5 @@
/* eslint-disable */
// This is an auto-generated file
export declare const card: string
export declare const cardsContainer: string
export declare const container: string
export declare const deploymentsCard: string
export declare const securityTestsCard: string

View File

@ -18,7 +18,7 @@ import React from 'react'
import classNames from 'classnames'
import { defaultTo } from 'lodash-es'
import { useHistory } from 'react-router-dom'
import { Layout, Page } from '@harnessio/uicore'
import { Container, Page } from '@harnessio/uicore'
import { useGetDockerArtifactIntegrationDetailsQuery } from '@harnessio/react-har-service-client'
import { encodeRef } from '@ar/hooks/useGetSpaceRef'
@ -81,9 +81,9 @@ export default function DockerVersionOverviewCards() {
retryOnError={() => refetch()}
error={typeof error === 'string' ? error : error?.message}>
{responseData && (
<Layout.Horizontal width="100%" spacing="medium">
<Container className={css.cardsContainer} width="100%">
<DeploymentsCard
className={classNames(css.card, css.deploymentsCard)}
className={classNames(css.card)}
onClick={() => {
handleRedirectToTab(VersionDetailsTab.DEPLOYMENTS)
}}
@ -100,15 +100,15 @@ export default function DockerVersionOverviewCards() {
artifactId: responseData.sbomDetails?.artifactId
})
}}
provenanceId={defaultTo(responseData.slsaDetails?.provenanceId, '')}
className={css.card}
orchestrationId={defaultTo(responseData.sbomDetails?.orchestrationId, '')}
className={classNames(css.card)}
totalComponents={defaultTo(responseData.sbomDetails?.componentsCount, 0)}
allowListCount={defaultTo(responseData.sbomDetails?.allowListViolations, 0)}
denyListCount={defaultTo(responseData.sbomDetails?.denyListViolations, 0)}
sbomScore={defaultTo(responseData.sbomDetails?.avgScore, 0)}
/>
<SecurityTestsCard
className={classNames(css.card, css.securityTestsCard)}
className={classNames(css.card)}
onClick={() => {
handleRedirectToTab(VersionDetailsTab.SECURITY_TESTS, {
executionIdentifier: responseData?.stoDetails?.executionId,
@ -139,7 +139,7 @@ export default function DockerVersionOverviewCards() {
}
]}
/>
</Layout.Horizontal>
</Container>
)}
</Page.Body>
)

View File

@ -16,5 +16,12 @@
.container {
display: flex;
gap: 20px;
gap: var(--spacing-medium);
}
.buildDetailsContainer {
--layout-spacing: var(--spacing-none) !important;
flex-shrink: 1 !important;
border-left: 1px solid var(--grey-200);
padding-left: var(--spacing-medium) !important;
}

View File

@ -16,4 +16,5 @@
/* eslint-disable */
// This is an auto-generated file
export declare const buildDetailsContainer: string
export declare const container: string

View File

@ -62,39 +62,46 @@ export default function DeploymentsCard(props: DeploymentsCardProps) {
{
label: getString('prod'),
value: defaultTo(prodCount, 0),
color: Color.PURPLE_900
color: Color.BLUE_900,
backgroundColor: Color.BLUE_50
},
{
label: getString('nonProd'),
value: defaultTo(nonProdCount, 0),
color: Color.TEAL_800
color: Color.TEAL_900,
backgroundColor: Color.TEAL_50
}
]}
/>
</Layout.Horizontal>
</Layout.Vertical>
{!hideBuildDetails && (
<Layout.Vertical spacing="medium" flex={{ justifyContent: 'space-between', alignItems: 'flex-start' }}>
<Layout.Vertical
className={css.buildDetailsContainer}
spacing="medium"
flex={{ justifyContent: 'space-between', alignItems: 'flex-start' }}>
<Text font={{ variation: FontVariation.CARD_TITLE }}>
{getString('versionDetails.cards.deploymentsCard.buildTitle')}
</Text>
{getRouteToPipelineExecutionView && pipelineName && executionId && pipelineId ? (
<Layout.Vertical spacing="xsmall">
<Link
to={getRouteToPipelineExecutionView({
accountId: scope.accountId,
orgIdentifier: scope.orgIdentifier,
projectIdentifier: scope.projectIdentifier,
pipelineIdentifier: pipelineId,
executionIdentifier: executionId,
module: 'ci'
})}
onClick={evt => {
evt.stopPropagation()
}}>
{pipelineName}
</Link>
<Text color={Color.GREY_500} font={{ variation: FontVariation.SMALL }}>
<Text lineClamp={1}>
<Link
to={getRouteToPipelineExecutionView({
accountId: scope.accountId,
orgIdentifier: scope.orgIdentifier,
projectIdentifier: scope.projectIdentifier,
pipelineIdentifier: pipelineId,
executionIdentifier: executionId,
module: 'ci'
})}
onClick={evt => {
evt.stopPropagation()
}}>
{pipelineName}
</Link>
</Text>
<Text color={Color.GREY_500} font={{ variation: FontVariation.SMALL }} lineClamp={1}>
{getString('versionDetails.cards.deploymentsCard.executionId')}: {executionId}
</Text>
</Layout.Vertical>

View File

@ -44,10 +44,10 @@ export const EnvironmentNameCell: CellType = ({ row }) => {
const { getString } = useStrings()
return (
<Layout.Vertical>
<Text color={Color.GREY_900} font={{ variation: FontVariation.BODY }}>
<Text lineClamp={1} color={Color.GREY_900} font={{ variation: FontVariation.BODY }}>
{envName}
</Text>
<Text color={Color.GREY_500} font={{ variation: FontVariation.SMALL }}>
<Text lineClamp={1} color={Color.GREY_500} font={{ variation: FontVariation.SMALL }}>
{getString('id', { id: envIdentifier })}
</Text>
</Layout.Vertical>
@ -73,20 +73,22 @@ export const ServiceListCell: CellType = ({ row }) => {
const { getRouteToServiceDetailsView } = useParentUtils()
if (getRouteToServiceDetailsView && serviceIdentifier) {
return (
<Link
key={serviceIdentifier}
to={getRouteToServiceDetailsView({
accountId: scope.accountId,
orgIdentifier: scope.orgIdentifier,
projectIdentifier: scope.projectIdentifier,
serviceId: serviceIdentifier
})}>
{serviceName}
</Link>
<Text lineClamp={1}>
<Link
key={serviceIdentifier}
to={getRouteToServiceDetailsView({
accountId: scope.accountId,
orgIdentifier: scope.orgIdentifier,
projectIdentifier: scope.projectIdentifier,
serviceId: serviceIdentifier
})}>
{serviceName}
</Link>
</Text>
)
}
return (
<Text key={serviceIdentifier} color={Color.GREY_900} font={{ variation: FontVariation.BODY }}>
<Text key={serviceIdentifier} lineClamp={1} color={Color.GREY_900} font={{ variation: FontVariation.BODY }}>
{serviceName}
</Text>
)
@ -101,23 +103,25 @@ export const DeploymentPipelineCell: CellType = ({ row }) => {
return (
<Layout.Vertical>
{pipelineId && getRouteToPipelineExecutionView ? (
<Link
to={getRouteToPipelineExecutionView({
accountId: scope.accountId,
orgIdentifier: scope.orgIdentifier,
projectIdentifier: scope.projectIdentifier,
pipelineIdentifier: defaultTo(pipelineId, ''),
executionIdentifier: defaultTo(lastPipelineExecutionId, ''),
module: 'cd'
})}>
{lastPipelineExecutionName}
</Link>
<Text lineClamp={1}>
<Link
to={getRouteToPipelineExecutionView({
accountId: scope.accountId,
orgIdentifier: scope.orgIdentifier,
projectIdentifier: scope.projectIdentifier,
pipelineIdentifier: defaultTo(pipelineId, ''),
executionIdentifier: defaultTo(lastPipelineExecutionId, ''),
module: 'cd'
})}>
{lastPipelineExecutionName}
</Link>
</Text>
) : (
<Text color={Color.GREY_900} font={{ variation: FontVariation.BODY }}>
{lastPipelineExecutionName}
</Text>
)}
<Text color={Color.GREY_500} font={{ variation: FontVariation.SMALL }}>
<Text lineClamp={1} color={Color.GREY_500} font={{ variation: FontVariation.SMALL }}>
{getString('id', { id: pipelineId })}
</Text>
</Layout.Vertical>

View File

@ -26,10 +26,12 @@ interface SecurityTestItemProps {
title: string
status: SecurityTestSatus
value: number | string
toPrecision?: number
}
export default function SecurityItem(props: SecurityTestItemProps) {
const { title, value, status } = props
const { title, value, status, toPrecision = 0 } = props
const formattedValue = new Number(value).toFixed(toPrecision)
return (
<Layout.Horizontal spacing="small" flex={{ alignItems: 'center', justifyContent: 'flex-start' }}>
<Text font={{ variation: FontVariation.SMALL }}>{title}</Text>
@ -43,7 +45,7 @@ export default function SecurityItem(props: SecurityTestItemProps) {
[css.green]: status === SecurityTestSatus.Green,
[css.info]: status === SecurityTestSatus.Info
})}>
{value}
{formattedValue}
</Text>
</Layout.Horizontal>
)

View File

@ -45,13 +45,5 @@
}
.container {
--layout-spacing: var(--spacing-medium) !important;
& .column:not(:first-child) {
margin-left: var(--spacing-medium) !important;
padding-left: var(--spacing-medium) !important;
border-left: 1px solid var(--grey-200);
&.primaryColumn {
border-left-color: var(--primary-700) !important;
}
}
--layout-spacing: var(--spacing-small) !important;
}

View File

@ -16,7 +16,6 @@
/* eslint-disable */
// This is an auto-generated file
export declare const column: string
export declare const container: string
export declare const critical: string
export declare const green: string
@ -24,5 +23,4 @@ export declare const high: string
export declare const info: string
export declare const low: string
export declare const medium: string
export declare const primaryColumn: string
export declare const statusValue: string

View File

@ -15,7 +15,7 @@
*/
import React from 'react'
import { FontVariation } from '@harnessio/design-system'
import { Color, FontVariation } from '@harnessio/design-system'
import { Card, Layout, Text } from '@harnessio/uicore'
import { useStrings } from '@ar/frameworks/strings'
@ -43,19 +43,19 @@ export default function SecurityTestsCard(props: SecurityTestsCardProps) {
<Text font={{ variation: FontVariation.CARD_TITLE }}>
{title ?? getString('versionDetails.cards.securityTests.title')}
</Text>
<Layout.Horizontal className={css.container}>
<Layout.Vertical className={css.column}>
<Layout.Vertical className={css.container}>
<Layout.Horizontal spacing="small" flex={{ alignItems: 'flex-end', justifyContent: 'flex-start' }}>
<Text font={{ variation: FontVariation.H2 }}>{totalCount}</Text>
<Text font={{ variation: FontVariation.SMALL }}>
{getString('versionDetails.cards.securityTests.totalCount')}
<Text color={Color.GREY_500} font={{ variation: FontVariation.SMALL }}>
{getString('versionDetails.cards.securityTests.totalIssues')}
</Text>
</Layout.Vertical>
<Layout.Vertical className={css.column} spacing="small">
</Layout.Horizontal>
<Layout.Horizontal spacing="small">
{items.map(each => (
<SecurityItem key={each.value} title={each.title} status={each.status} value={each.value} />
))}
</Layout.Vertical>
</Layout.Horizontal>
</Layout.Horizontal>
</Layout.Vertical>
</Layout.Vertical>
</Card>
)

View File

@ -15,15 +15,7 @@
*/
.container {
--layout-spacing: var(--spacing-medium) !important;
& .column:not(:first-child) {
margin-left: var(--spacing-medium) !important;
padding-left: var(--spacing-medium) !important;
border-left: 1px solid var(--grey-200);
&.primaryColumn {
border-left-color: var(--primary-700) !important;
}
}
--layout-spacing: var(--spacing-small) !important;
}
.downloadSlsaBtn {

View File

@ -16,7 +16,5 @@
/* eslint-disable */
// This is an auto-generated file
export declare const column: string
export declare const container: string
export declare const downloadSlsaBtn: string
export declare const primaryColumn: string

View File

@ -15,14 +15,13 @@
*/
import React from 'react'
import classNames from 'classnames'
import { Button, ButtonSize, ButtonVariation, Card, Layout, Text } from '@harnessio/uicore'
import { Color, FontVariation } from '@harnessio/design-system'
import { killEvent } from '@ar/common/utils'
import { useStrings } from '@ar/frameworks/strings'
import useDownloadSLSAProvenance from '@ar/pages/version-details/hooks/useDownloadSLSAProvenance'
import useDownloadSBOM from '../../hooks/useDownloadSBOM'
import SecurityItem from '../SecurityTestsCard/SecurityItem'
import { SecurityTestSatus } from '../SecurityTestsCard/types'
@ -35,15 +34,15 @@ interface SupplyChainCardProps {
allowListCount: number
denyListCount: number
sbomScore: string | number
provenanceId: string
orchestrationId: string
onClick?: () => void
}
export default function SupplyChainCard(props: SupplyChainCardProps) {
const { title, totalComponents, allowListCount, denyListCount, className, sbomScore, onClick, provenanceId } = props
const { title, totalComponents, className, sbomScore, onClick, orchestrationId } = props
const { getString } = useStrings()
const { download, loading } = useDownloadSLSAProvenance()
const { download, loading } = useDownloadSBOM()
return (
<Card className={className} onClick={onClick}>
@ -51,18 +50,19 @@ export default function SupplyChainCard(props: SupplyChainCardProps) {
<Text font={{ variation: FontVariation.CARD_TITLE }}>
{title ?? getString('versionDetails.cards.supplyChain.title')}
</Text>
<Layout.Horizontal className={css.container}>
<Layout.Vertical className={css.column}>
<Layout.Vertical className={css.container}>
<Layout.Horizontal spacing="small" flex={{ alignItems: 'flex-end', justifyContent: 'flex-start' }}>
<Text font={{ variation: FontVariation.H2 }}>{totalComponents}</Text>
<Text font={{ variation: FontVariation.SMALL }}>
<Text color={Color.GREY_500} font={{ variation: FontVariation.SMALL }}>
{getString('versionDetails.cards.supplyChain.totalComponents')}
</Text>
</Layout.Vertical>
<Layout.Vertical className={css.column} spacing="small">
</Layout.Horizontal>
<Layout.Horizontal spacing="small">
<SecurityItem
title={getString('versionDetails.cards.supplyChain.sbomScore')}
value={sbomScore}
status={SecurityTestSatus.Green}
toPrecision={2}
/>
<Button
className={css.downloadSlsaBtn}
@ -70,31 +70,15 @@ export default function SupplyChainCard(props: SupplyChainCardProps) {
rightIcon="download-manifests"
variation={ButtonVariation.LINK}
loading={loading}
disabled={!provenanceId}
disabled={!orchestrationId}
onClick={evt => {
killEvent(evt)
download(provenanceId)
download(orchestrationId)
}}>
{getString('versionDetails.cards.supplyChain.slsaProvenance')}
{getString('versionDetails.cards.supplyChain.downloadSbom')}
</Button>
</Layout.Vertical>
</Layout.Horizontal>
<Layout.Horizontal className={css.container}>
<Text
className={css.column}
color={Color.PRIMARY_7}
font={{ variation: FontVariation.SMALL }}
icon="danger-icon"
iconProps={{ size: 18 }}>
{allowListCount} {getString('versionDetails.cards.supplyChain.allowList')}
</Text>
<Text
className={classNames(css.column, css.primaryColumn)}
color={Color.PRIMARY_7}
font={{ variation: FontVariation.SMALL }}>
{denyListCount} {getString('versionDetails.cards.supplyChain.denyListViolation')}
</Text>
</Layout.Horizontal>
</Layout.Horizontal>
</Layout.Vertical>
</Layout.Vertical>
</Card>
)

View File

@ -0,0 +1,56 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState } from 'react'
import { defaultTo } from 'lodash-es'
import { useToaster } from '@harnessio/uicore'
import { downloadSbom } from '@harnessio/react-ssca-manager-client'
import { useAppStore } from '@ar/hooks'
import { useStrings } from '@ar/frameworks/strings'
import { downloadRawFile } from '@ar/utils/downloadRawFile'
export default function useDownloadSBOM() {
const [loading, setLoading] = useState(false)
const { scope } = useAppStore()
const { showError } = useToaster()
const { getString } = useStrings()
const download = async (orchestrationId: string) => {
setLoading(true)
return downloadSbom({
org: defaultTo(scope.orgIdentifier, ''),
project: defaultTo(scope.projectIdentifier, ''),
'orchestration-id': orchestrationId
})
.then(data => {
const content = defaultTo(data.content.sbom, '')
if (!content) {
throw new Error(getString('versionDetails.cards.supplyChain.SBOMDataNotAvailable'))
}
return downloadRawFile(content, `sbom_${orchestrationId}.json`)
})
.catch((err: Error) => {
showError(defaultTo(err?.message, err))
return false
})
.finally(() => {
setLoading(false)
})
}
return { download, loading }
}

View File

@ -39,7 +39,7 @@ export default function useDownloadSLSAProvenance() {
.then(data => {
const content = defaultTo(data.content.provenance, '')
if (!content) {
throw new Error(getString('versionDetails.cards.supplyChain.provenanceDataNotAvailable'))
throw new Error(getString('versionDetails.cards.slsaCard.provenanceDataNotAvailable'))
}
return downloadRawFile(content, `provenance_${provenanceId}.json`)
})

View File

@ -14,19 +14,24 @@ cards:
executionId: Execution ID
securityTests:
title: Vulnerabilities
totalCount: Total Count
totalIssues: Total Issues
critical: Critical
high: High
medium: Medium
low: Low
info: Info
ignored: Ignored
slsaCard:
title: SLSA
slsaVerificationStatus: SLSA Verification
slsaProvenance: SLSA Provenance
provenanceDataNotAvailable: Provenance data is not available
supplyChain:
title: SBOM
totalComponents: Total Dependencies
sbomScore: SBOM Score
slsaProvenance: SLSA Provenance
provenanceDataNotAvailable: Provenance data is not available
sbomScore: SBOM Quality Score
downloadSbom: Download SBOM
SBOMDataNotAvailable: SBOM data is not available
allowList: Allow List
denyListViolation: Deny List Violation
container:

View File

@ -164,12 +164,16 @@ export interface StringsMap {
'versionDetails.cards.securityTests.low': string
'versionDetails.cards.securityTests.medium': string
'versionDetails.cards.securityTests.title': string
'versionDetails.cards.securityTests.totalCount': string
'versionDetails.cards.securityTests.totalIssues': string
'versionDetails.cards.slsaCard.provenanceDataNotAvailable': string
'versionDetails.cards.slsaCard.slsaProvenance': string
'versionDetails.cards.slsaCard.slsaVerificationStatus': string
'versionDetails.cards.slsaCard.title': string
'versionDetails.cards.supplyChain.SBOMDataNotAvailable': string
'versionDetails.cards.supplyChain.allowList': string
'versionDetails.cards.supplyChain.denyListViolation': string
'versionDetails.cards.supplyChain.provenanceDataNotAvailable': string
'versionDetails.cards.supplyChain.downloadSbom': string
'versionDetails.cards.supplyChain.sbomScore': string
'versionDetails.cards.supplyChain.slsaProvenance': string
'versionDetails.cards.supplyChain.title': string
'versionDetails.cards.supplyChain.totalComponents': string
'versionDetails.deploymentsTable.columns.deploymentPipeline': string