diff --git a/web/src/components/ExecutionStatus/ExecutionStatus.tsx b/web/src/components/ExecutionStatus/ExecutionStatus.tsx index fbb3492b1..7477e05bb 100644 --- a/web/src/components/ExecutionStatus/ExecutionStatus.tsx +++ b/web/src/components/ExecutionStatus/ExecutionStatus.tsx @@ -42,7 +42,8 @@ interface ExecutionStatusProps { } export enum ExecutionStateExtended { - FAILED = 'failed' + FAILED = 'failed', + ABORTED = 'aborted' } export const ExecutionStatus: React.FC = ({ @@ -96,12 +97,16 @@ export const ExecutionStatus: React.FC = ({ 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 ( \ No newline at end of file diff --git a/web/src/pages/PullRequest/Checks/Checks.module.scss b/web/src/pages/PullRequest/Checks/Checks.module.scss index 8d102a999..46186e630 100644 --- a/web/src/pages/PullRequest/Checks/Checks.module.scss +++ b/web/src/pages/PullRequest/Checks/Checks.module.scss @@ -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; + } + } + } +} diff --git a/web/src/pages/PullRequest/Checks/Checks.module.scss.d.ts b/web/src/pages/PullRequest/Checks/Checks.module.scss.d.ts index 7add0dcee..aa098ee75 100644 --- a/web/src/pages/PullRequest/Checks/Checks.module.scss.d.ts +++ b/web/src/pages/PullRequest/Checks/Checks.module.scss.d.ts @@ -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 diff --git a/web/src/pages/PullRequest/Checks/ChecksMenu.tsx b/web/src/pages/PullRequest/Checks/ChecksMenu.tsx index 1789ad541..760eccb0e 100644 --- a/web/src/pages/PullRequest/Checks/ChecksMenu.tsx +++ b/web/src/pages/PullRequest/Checks/ChecksMenu.tsx @@ -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 = ({ repoMetadata, pullRequestMetadata, @@ -43,13 +56,12 @@ export const ChecksMenu: React.FC = ({ onDataItemChanged, setSelectedStage: setSelectedStageFromProps }) => { - const { routes } = useAppContext() + const { routes, standalone } = useAppContext() const history = useHistory() const { uid } = useQueryParams<{ uid: string }>() const [selectedUID, setSelectedUID] = React.useState() const [selectedStage, setSelectedStage] = useState(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 = ({ 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 ( - - {checksData.map(itemData => ( - { - setSelectedUID(itemData.uid) - setSelectedStage(stage || null) - setSelectedStageFromProps(stage || null) + + {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} + {Object.entries(groupedData).map(([pipelineId, checks]: any) => ( + { + 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-') && ( + + + + + + {pipelineId} + + + + + {statusTimeStates[pipelineId]?.time} + + + + + )} + {(checks as TypesCheck[]).map((itemData: TypesCheck) => ( + { + 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) + }} + /> + ))} + ))} - + ) } @@ -149,7 +274,12 @@ const CheckMenuItem: React.FC = ({ 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 ( = ({ [css.forPipeline]: isPipeline })} {...ButtonRoleProps} - onClick={() => { + onClick={e => { + e.stopPropagation() if (isPipeline) { setExpanded(!expanded) } else { onClick() } }}> - - - - + - {itemData.uid} + {name} @@ -185,13 +315,13 @@ const CheckMenuItem: React.FC = ({ {timeDistance(itemData.updated, itemData.created)} - + + +