mirror of
https://github.com/harness/drone.git
synced 2025-05-21 11:29:52 +08:00
feat: [code-1195]: add enhance ments for ci integration in left pane (#931)
This commit is contained in:
parent
c0bca6eea1
commit
7118dd6f83
@ -42,7 +42,8 @@ interface ExecutionStatusProps {
|
||||
}
|
||||
|
||||
export enum ExecutionStateExtended {
|
||||
FAILED = 'failed'
|
||||
FAILED = 'failed',
|
||||
ABORTED = 'aborted'
|
||||
}
|
||||
|
||||
export const ExecutionStatus: React.FC<ExecutionStatusProps> = ({
|
||||
@ -96,12 +97,16 @@ export const ExecutionStatus: React.FC<ExecutionStatusProps> = ({
|
||||
icon: 'execution-stopped',
|
||||
css: null,
|
||||
title: getString('killed').toLocaleUpperCase()
|
||||
},
|
||||
[ExecutionStateExtended.ABORTED]: {
|
||||
icon: 'execution-stopped',
|
||||
css: null,
|
||||
title: getString('killed').toLocaleUpperCase()
|
||||
}
|
||||
}),
|
||||
[getString, inExecution, isCi]
|
||||
)
|
||||
const map = useMemo(() => maps[status], [maps, status])
|
||||
|
||||
return (
|
||||
<Text
|
||||
tag="span"
|
||||
|
1
web/src/icons/Ci-main.svg
Normal file
1
web/src/icons/Ci-main.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 32 32"><path fill="url(#a)" fill-rule="evenodd" d="M29 16a13.02 13.02 0 0 1-.854 4.6.387.387 0 0 1-.637.13l-3.99-3.94a.534.534 0 0 1-.085-.628c.432-.827.649-1.773.649-2.762.056-3.222-2.6-5.878-5.879-5.878-1.04 0-1.994.26-2.823.706a.53.53 0 0 1-.623-.08l-3.551-3.504a.48.48 0 0 1 .154-.794A12.79 12.79 0 0 1 16 3c7.178 0 13 5.822 13 13M8.476 5.42a.582.582 0 0 1 .746.072l4.036 4.035c.178.178.193.46.053.67a5.829 5.829 0 0 0-.985 3.26 5.934 5.934 0 0 0 5.935 5.934 5.829 5.829 0 0 0 3.26-.985.531.531 0 0 1 .67.054l4.367 4.368a.582.582 0 0 1 .065.756C24.225 26.86 20.4 29 16 29 8.822 29 3 23.178 3 16c0-4.403 2.144-8.231 5.476-10.58Zm9.785 11.71a3.932 3.932 0 0 0 3.956-3.956 3.932 3.932 0 0 0-3.956-3.957 3.932 3.932 0 0 0-3.957 3.957 3.932 3.932 0 0 0 3.957 3.956" clip-rule="evenodd"/><defs><linearGradient id="a" x1="-5.398" x2="11.398" y1="11.398" y2="37.398" gradientUnits="userSpaceOnUse"><stop stop-color="#73DFE7"/><stop offset="1" stop-color="#0095F7"/></linearGradient></defs></svg>
|
After Width: | Height: | Size: 1.0 KiB |
@ -38,6 +38,36 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.leftPaneContent {
|
||||
align-items: center !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--primary-bg);
|
||||
.leftPaneMenuItem {
|
||||
width: 350px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 0.5px 2px 0px rgba(96, 97, 112, 0.16), 0px 0px 1px 0px rgba(40, 41, 61, 0.08);
|
||||
background: var(--grey-0);
|
||||
|
||||
&.expanded {
|
||||
.chevron {
|
||||
transform: rotate(90deg) !important;
|
||||
}
|
||||
.menuItem {
|
||||
border-bottom: unset !important;
|
||||
> .layout {
|
||||
padding: 0px var(--spacing-xxxlarge);
|
||||
border-bottom: unset !important;
|
||||
> .uid {
|
||||
color: var(--grey-600) !important;
|
||||
font-weight: 500 !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
.menuItem {
|
||||
@ -302,3 +332,21 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hideStages {
|
||||
> .menuItem {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.bp3-popover-wrapper {
|
||||
> .bp3-popover-target {
|
||||
> .uid {
|
||||
color: var(--grey-600) !important;
|
||||
font-weight: 500 !important;
|
||||
font-size: 13px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,8 +25,11 @@ export declare const forPipeline: string
|
||||
export declare const header: string
|
||||
export declare const headerLayout: string
|
||||
export declare const hidden: string
|
||||
export declare const hideStages: string
|
||||
export declare const invert: string
|
||||
export declare const layout: string
|
||||
export declare const leftPaneContent: string
|
||||
export declare const leftPaneMenuItem: string
|
||||
export declare const logViewer: string
|
||||
export declare const main: string
|
||||
export declare const markdown: string
|
||||
|
@ -22,10 +22,16 @@ import cx from 'classnames'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { Container, Layout, Text, FlexExpander, Utils } from '@harnessio/uicore'
|
||||
import { Color, FontVariation } from '@harnessio/design-system'
|
||||
import { ButtonRoleProps, PullRequestCheckType, PullRequestSection, timeDistance } from 'utils/Utils'
|
||||
import {
|
||||
ButtonRoleProps,
|
||||
PullRequestCheckType,
|
||||
PullRequestSection,
|
||||
generateAlphaNumericHash,
|
||||
timeDistance
|
||||
} from 'utils/Utils'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { useQueryParams } from 'hooks/useQueryParams'
|
||||
import type { TypesCheck, TypesStage } from 'services/code'
|
||||
import type { EnumCheckPayloadKind, TypesCheck, TypesStage } from 'services/code'
|
||||
import { ExecutionState, ExecutionStatus } from 'components/ExecutionStatus/ExecutionStatus'
|
||||
import { CheckPipelineStages } from './CheckPipelineStages'
|
||||
import { ChecksProps, findDefaultExecution } from './ChecksUtils'
|
||||
@ -36,6 +42,13 @@ interface ChecksMenuProps extends ChecksProps {
|
||||
setSelectedStage: (stage: TypesStage | null) => void
|
||||
}
|
||||
|
||||
type TypesCheckPayloadExtended = EnumCheckPayloadKind | 'harness_stage'
|
||||
type ExpandedStates = { [key: string]: boolean }
|
||||
type ElapsedTimeStatusMap = { [key: string]: { status: 'string'; time: string } }
|
||||
|
||||
enum CheckKindPayload {
|
||||
HARNESS_STAGE = 'harness_stage'
|
||||
}
|
||||
export const ChecksMenu: React.FC<ChecksMenuProps> = ({
|
||||
repoMetadata,
|
||||
pullRequestMetadata,
|
||||
@ -43,13 +56,12 @@ export const ChecksMenu: React.FC<ChecksMenuProps> = ({
|
||||
onDataItemChanged,
|
||||
setSelectedStage: setSelectedStageFromProps
|
||||
}) => {
|
||||
const { routes } = useAppContext()
|
||||
const { routes, standalone } = useAppContext()
|
||||
const history = useHistory()
|
||||
const { uid } = useQueryParams<{ uid: string }>()
|
||||
const [selectedUID, setSelectedUID] = React.useState<string | undefined>()
|
||||
const [selectedStage, setSelectedStage] = useState<TypesStage | null>(null)
|
||||
const checksData = useMemo(() => sortBy(prChecksDecisionResult?.data || [], ['uid']), [prChecksDecisionResult?.data])
|
||||
|
||||
useMemo(() => {
|
||||
if (selectedUID) {
|
||||
const selectedDataItem = checksData.find(item => item.uid === selectedUID)
|
||||
@ -90,38 +102,151 @@ export const ChecksMenu: React.FC<ChecksMenuProps> = ({
|
||||
onDataItemChanged,
|
||||
selectedStage
|
||||
])
|
||||
const [expandedStates, setExpandedStates] = useState<{ [key: string]: boolean }>({})
|
||||
const [statusTimeStates, setStatusTimeStates] = useState<{ [key: string]: { status: string; time: string } }>({})
|
||||
|
||||
const groupByPipeline = (data: TypesCheck[]) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return data.reduce((acc: any, item: any) => {
|
||||
const hash = generateAlphaNumericHash(6)
|
||||
|
||||
const pipelineId =
|
||||
(item?.payload?.kind as TypesCheckPayloadExtended) === CheckKindPayload.HARNESS_STAGE
|
||||
? item?.payload?.data?.pipeline_identifier
|
||||
: `raw-${hash}`
|
||||
|
||||
if (!acc[pipelineId]) {
|
||||
acc[pipelineId] = []
|
||||
}
|
||||
acc[pipelineId].push(item)
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
const toggleExpandedState = (key: string) => {
|
||||
setExpandedStates(prevStates => ({
|
||||
...prevStates,
|
||||
[key]: !prevStates[key]
|
||||
}))
|
||||
}
|
||||
const groupedData = useMemo(() => groupByPipeline(checksData), [checksData])
|
||||
useEffect(() => {
|
||||
const initialStates: ExpandedStates = {}
|
||||
const initialMap: ElapsedTimeStatusMap = {}
|
||||
|
||||
Object.keys(groupedData).forEach(key => {
|
||||
const findStatus = () => {
|
||||
const statusPriority = ['running', 'failure', 'error', 'success']
|
||||
|
||||
for (const status of statusPriority) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const foundObject = groupedData[key].find((obj: any) => obj.status === status)
|
||||
if (foundObject) return foundObject
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const dataArr = groupedData[key]
|
||||
if (groupedData && dataArr) {
|
||||
let startTime = 0
|
||||
let endTime = 0
|
||||
dataArr.map((item: TypesCheck) => {
|
||||
if (item) {
|
||||
startTime += item.created ? item?.created : 0
|
||||
endTime += item.updated ? item?.updated : 0
|
||||
}
|
||||
})
|
||||
const res = findStatus()
|
||||
initialMap[key] = { status: res.status, time: timeDistance(startTime, endTime) }
|
||||
}
|
||||
if (uid) {
|
||||
initialStates[key] = uid.includes(key) ? true : false // or true if you want them initially expanded
|
||||
} else {
|
||||
initialStates[key] = false
|
||||
}
|
||||
})
|
||||
setStatusTimeStates(initialMap)
|
||||
setExpandedStates(initialStates)
|
||||
}, [groupedData, uid])
|
||||
return (
|
||||
<Container className={css.menu}>
|
||||
{checksData.map(itemData => (
|
||||
<CheckMenuItem
|
||||
repoMetadata={repoMetadata}
|
||||
pullRequestMetadata={pullRequestMetadata}
|
||||
prChecksDecisionResult={prChecksDecisionResult}
|
||||
key={itemData.uid}
|
||||
itemData={itemData}
|
||||
isPipeline={itemData.payload?.kind === PullRequestCheckType.PIPELINE}
|
||||
isSelected={itemData.uid === selectedUID}
|
||||
onClick={stage => {
|
||||
setSelectedUID(itemData.uid)
|
||||
setSelectedStage(stage || null)
|
||||
setSelectedStageFromProps(stage || null)
|
||||
<Layout.Vertical padding={{ top: 'large' }} spacing={'small'} className={cx(css.menu, css.leftPaneContent)}>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
||||
{Object.entries(groupedData).map(([pipelineId, checks]: any) => (
|
||||
<Container
|
||||
key={pipelineId}
|
||||
onClick={() => {
|
||||
toggleExpandedState(pipelineId)
|
||||
}}
|
||||
className={cx(css.leftPaneMenuItem, {
|
||||
[css.expanded]: expandedStates[pipelineId] && !pipelineId.includes('raw-') && !standalone,
|
||||
[css.layout]: !pipelineId.includes('raw-') && !standalone,
|
||||
[css.menuItem]: !pipelineId.includes('raw-') && !standalone,
|
||||
[css.hideStages]: !expandedStates[pipelineId] && !pipelineId.includes('raw-') && !standalone
|
||||
})}>
|
||||
{!standalone && !pipelineId.includes('raw-') && (
|
||||
<Layout.Horizontal className={css.layout}>
|
||||
<Render when={statusTimeStates[pipelineId]?.status}>
|
||||
<ExecutionStatus
|
||||
className={cx(css.status, css.noShrink)}
|
||||
status={statusTimeStates[pipelineId]?.status as ExecutionState}
|
||||
iconSize={16}
|
||||
noBackground
|
||||
iconOnly
|
||||
/>
|
||||
</Render>
|
||||
<Text className={css.uid} lineClamp={1} padding={{ left: 'small' }}>
|
||||
{pipelineId}
|
||||
</Text>
|
||||
<FlexExpander />
|
||||
<Render when={statusTimeStates[pipelineId]?.time}>
|
||||
<Text
|
||||
color={Color.GREY_300}
|
||||
padding={{ right: 'small' }}
|
||||
font={{ variation: FontVariation.SMALL }}
|
||||
className={css.noShrink}>
|
||||
{statusTimeStates[pipelineId]?.time}
|
||||
</Text>
|
||||
</Render>
|
||||
<NavArrowRight
|
||||
color={Utils.getRealCSSColor(Color.GREY_500)}
|
||||
className={cx(css.noShrink, css.chevron)}
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</Layout.Horizontal>
|
||||
)}
|
||||
{(checks as TypesCheck[]).map((itemData: TypesCheck) => (
|
||||
<CheckMenuItem
|
||||
repoMetadata={repoMetadata}
|
||||
pullRequestMetadata={pullRequestMetadata}
|
||||
prChecksDecisionResult={prChecksDecisionResult}
|
||||
key={itemData.uid}
|
||||
itemData={itemData}
|
||||
isPipeline={itemData.payload?.kind === PullRequestCheckType.PIPELINE}
|
||||
isSelected={itemData.uid === selectedUID}
|
||||
onClick={stage => {
|
||||
setSelectedUID(itemData.uid)
|
||||
setSelectedStage(stage || null)
|
||||
setSelectedStageFromProps(stage || null)
|
||||
|
||||
history.replace(
|
||||
routes.toCODEPullRequest({
|
||||
repoPath: repoMetadata.path as string,
|
||||
pullRequestId: String(pullRequestMetadata.number),
|
||||
pullRequestSection: PullRequestSection.CHECKS
|
||||
}) + `?uid=${itemData.uid}${stage ? `&stageId=${stage.name}` : ''}`
|
||||
)
|
||||
}}
|
||||
setSelectedStage={stage => {
|
||||
setSelectedStage(stage)
|
||||
setSelectedStageFromProps(stage)
|
||||
}}
|
||||
/>
|
||||
history.replace(
|
||||
routes.toCODEPullRequest({
|
||||
repoPath: repoMetadata.path as string,
|
||||
pullRequestId: String(pullRequestMetadata.number),
|
||||
pullRequestSection: PullRequestSection.CHECKS
|
||||
}) + `?uid=${itemData.uid}${stage ? `&stageId=${stage.name}` : ''}`
|
||||
)
|
||||
}}
|
||||
setSelectedStage={stage => {
|
||||
setSelectedStage(stage)
|
||||
setSelectedStageFromProps(stage)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Container>
|
||||
))}
|
||||
</Container>
|
||||
</Layout.Vertical>
|
||||
)
|
||||
}
|
||||
|
||||
@ -149,7 +274,12 @@ const CheckMenuItem: React.FC<CheckMenuItemProps> = ({
|
||||
setExpanded(isSelected)
|
||||
}
|
||||
}, [isSelected])
|
||||
|
||||
const name =
|
||||
itemData?.uid &&
|
||||
itemData?.uid.includes('-') &&
|
||||
(itemData.payload?.kind as TypesCheckPayloadExtended) === CheckKindPayload.HARNESS_STAGE
|
||||
? itemData.uid.split('-')[1]
|
||||
: itemData.uid
|
||||
return (
|
||||
<Container className={css.menuItem}>
|
||||
<Layout.Horizontal
|
||||
@ -160,23 +290,23 @@ const CheckMenuItem: React.FC<CheckMenuItemProps> = ({
|
||||
[css.forPipeline]: isPipeline
|
||||
})}
|
||||
{...ButtonRoleProps}
|
||||
onClick={() => {
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
if (isPipeline) {
|
||||
setExpanded(!expanded)
|
||||
} else {
|
||||
onClick()
|
||||
}
|
||||
}}>
|
||||
<Render when={isPipeline}>
|
||||
<NavArrowRight
|
||||
color={Utils.getRealCSSColor(Color.GREY_500)}
|
||||
className={cx(css.noShrink, css.chevron)}
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</Render>
|
||||
|
||||
<ExecutionStatus
|
||||
className={cx(css.status, css.noShrink)}
|
||||
status={itemData.status as ExecutionState}
|
||||
iconSize={16}
|
||||
noBackground
|
||||
iconOnly
|
||||
/>
|
||||
<Text className={css.uid} lineClamp={1}>
|
||||
{itemData.uid}
|
||||
{name}
|
||||
</Text>
|
||||
|
||||
<FlexExpander />
|
||||
@ -185,13 +315,13 @@ const CheckMenuItem: React.FC<CheckMenuItemProps> = ({
|
||||
{timeDistance(itemData.updated, itemData.created)}
|
||||
</Text>
|
||||
|
||||
<ExecutionStatus
|
||||
className={cx(css.status, css.noShrink)}
|
||||
status={itemData.status as ExecutionState}
|
||||
iconSize={16}
|
||||
noBackground
|
||||
iconOnly
|
||||
/>
|
||||
<Render when={isPipeline}>
|
||||
<NavArrowRight
|
||||
color={Utils.getRealCSSColor(Color.GREY_500)}
|
||||
className={cx(css.noShrink, css.chevron)}
|
||||
strokeWidth="1.5"
|
||||
/>
|
||||
</Render>
|
||||
</Layout.Horizontal>
|
||||
|
||||
<Render when={isPipeline}>
|
||||
|
Loading…
Reference in New Issue
Block a user