mirror of https://github.com/harness/drone.git
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 pagepull/3590/head
parent
9ce4ea8ee5
commit
37c7d3c626
|
@ -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(
|
||||
() => () => {
|
||||
|
|
|
@ -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}`)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
})}
|
||||
|
|
|
@ -21,4 +21,5 @@ export interface PieChartItem {
|
|||
value: number
|
||||
formattedValue?: string | React.ReactElement
|
||||
color?: Color
|
||||
backgroundColor?: Color
|
||||
}
|
||||
|
|
|
@ -45,5 +45,5 @@
|
|||
|
||||
.nonProdTag {
|
||||
background-color: var(--teal-50) !important;
|
||||
color: var(--team-900) !important;
|
||||
color: var(--teal-900) !important;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -16,4 +16,5 @@
|
|||
|
||||
/* eslint-disable */
|
||||
// This is an auto-generated file
|
||||
export declare const buildDetailsContainer: string
|
||||
export declare const container: string
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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 }
|
||||
}
|
|
@ -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`)
|
||||
})
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue