mirror of
https://github.com/harness/drone.git
synced 2025-05-17 01:20:13 +08:00
feat: [code-1362]: add streaming and other fixes in status checks (#1020)
This commit is contained in:
parent
e0f8248ead
commit
3a3d11f589
@ -43,7 +43,8 @@ interface ExecutionStatusProps {
|
||||
|
||||
export enum ExecutionStateExtended {
|
||||
FAILED = 'failed',
|
||||
ABORTED = 'aborted'
|
||||
ABORTED = 'aborted',
|
||||
ASYNCWAITING = 'asyncwaiting'
|
||||
}
|
||||
|
||||
export const ExecutionStatus: React.FC<ExecutionStatusProps> = ({
|
||||
@ -102,6 +103,11 @@ export const ExecutionStatus: React.FC<ExecutionStatusProps> = ({
|
||||
icon: 'execution-stopped',
|
||||
css: null,
|
||||
title: getString('killed').toLocaleUpperCase()
|
||||
},
|
||||
[ExecutionStateExtended.ASYNCWAITING]: {
|
||||
icon: 'running-filled',
|
||||
css: css.running,
|
||||
title: getString('running').toLocaleUpperCase()
|
||||
}
|
||||
}),
|
||||
[getString, inExecution, isCi]
|
||||
|
@ -132,3 +132,77 @@
|
||||
.noShrink {
|
||||
flex-shrink: inherit;
|
||||
}
|
||||
|
||||
.content {
|
||||
background-color: var(--black);
|
||||
overflow: auto;
|
||||
|
||||
&.markdown {
|
||||
:global {
|
||||
.wmde-markdown {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
padding: 0 var(--spacing-large) var(--spacing-medium);
|
||||
}
|
||||
|
||||
&.terminal {
|
||||
.header {
|
||||
padding: var(--spacing-medium) var(--spacing-large) 0;
|
||||
}
|
||||
|
||||
span[data-icon='execution-success'] svg {
|
||||
circle {
|
||||
color: transparent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-top: var(--spacing-medium);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: var(--black);
|
||||
height: var(--log-content-header-height);
|
||||
z-index: 3;
|
||||
|
||||
.headerLayout {
|
||||
border-bottom: 1px solid var(--grey-800);
|
||||
padding-bottom: var(--spacing-medium);
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.markdownContainer {
|
||||
padding-top: var(--spacing-medium);
|
||||
padding-left: var(--spacing-small);
|
||||
}
|
||||
|
||||
.logViewer {
|
||||
padding: var(--spacing-medium) var(--spacing-medium) var(--spacing-medium) var(--spacing-xxlarge);
|
||||
}
|
||||
}
|
||||
|
||||
.scrollDownBtn {
|
||||
position: absolute;
|
||||
padding: 8px !important;
|
||||
bottom: 7px;
|
||||
right: 30px;
|
||||
|
||||
& > :global(.bp3-icon) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
& > :global(.bp3-button-text) {
|
||||
width: 0;
|
||||
padding-left: 0;
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&:hover > :global(.bp3-button-text) {
|
||||
width: auto;
|
||||
padding-left: 4px;
|
||||
}
|
||||
}
|
||||
|
@ -18,15 +18,23 @@
|
||||
// This is an auto-generated file
|
||||
export declare const chevron: string
|
||||
export declare const consoleLine: string
|
||||
export declare const content: string
|
||||
export declare const expanded: string
|
||||
export declare const header: string
|
||||
export declare const headerLayout: string
|
||||
export declare const invert: string
|
||||
export declare const line: string
|
||||
export declare const logViewer: string
|
||||
export declare const main: string
|
||||
export declare const markdown: string
|
||||
export declare const markdownContainer: string
|
||||
export declare const name: string
|
||||
export declare const noShrink: string
|
||||
export declare const pipelineSteps: string
|
||||
export declare const scrollDownBtn: string
|
||||
export declare const selected: string
|
||||
export declare const status: string
|
||||
export declare const stepContainer: string
|
||||
export declare const stepHeader: string
|
||||
export declare const stepLogContainer: string
|
||||
export declare const terminal: string
|
||||
|
@ -14,20 +14,22 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import Anser from 'anser'
|
||||
import cx from 'classnames'
|
||||
import { Container, FlexExpander, Layout, Text, Utils } from '@harnessio/uicore'
|
||||
import { Button, ButtonSize, ButtonVariation, Container, FlexExpander, Layout, Text, Utils } from '@harnessio/uicore'
|
||||
import { NavArrowRight } from 'iconoir-react'
|
||||
import { isEmpty } from 'lodash-es'
|
||||
import { Color, FontVariation } from '@harnessio/design-system'
|
||||
import { Render } from 'react-jsx-match'
|
||||
import { parseLogString } from 'pages/PullRequest/Checks/ChecksUtils'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import { capitalizeFirstLetter, parseLogString } from 'pages/PullRequest/Checks/ChecksUtils'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { ButtonRoleProps, timeDistance } from 'utils/Utils'
|
||||
import { useScheduleJob } from 'hooks/useScheduleJob'
|
||||
import type { EnumCheckPayloadKind, TypesCheck, TypesStage } from 'services/code'
|
||||
import { ExecutionState, ExecutionStatus } from 'components/ExecutionStatus/ExecutionStatus'
|
||||
import css from './LogViewer.module.scss'
|
||||
|
||||
export interface LogViewerProps {
|
||||
search?: string
|
||||
content?: string
|
||||
@ -53,6 +55,11 @@ enum StepTypes {
|
||||
INITIALIZE = 'Initialize'
|
||||
}
|
||||
|
||||
enum StepStatus {
|
||||
RUNNING = 'running',
|
||||
SUCCESS = 'success'
|
||||
}
|
||||
|
||||
export type EnumCheckPayloadKindExtended = EnumCheckPayloadKind | 'harness_stage'
|
||||
|
||||
const LogTerminal: React.FC<LogViewerProps> = ({
|
||||
@ -63,9 +70,10 @@ const LogTerminal: React.FC<LogViewerProps> = ({
|
||||
selectedItemData
|
||||
}) => {
|
||||
const { hooks } = useAppContext()
|
||||
const { getString } = useStrings()
|
||||
|
||||
const ref = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
const containerKey = selectedItemData ? selectedItemData.id : 'default-key'
|
||||
useEffect(() => {
|
||||
// Clear the container first
|
||||
@ -82,50 +90,94 @@ const LogTerminal: React.FC<LogViewerProps> = ({
|
||||
}
|
||||
}, [content, stepNameLogKeyMap, selectedItemData])
|
||||
|
||||
const getLogData = (logBaseKey: string) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const getLogData = (logBaseKey: string, status: string, onMessageStreaming: (e: any) => void) => {
|
||||
const logContent = hooks?.useLogsContent([logBaseKey])
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const onError = (e: any) => {
|
||||
if (e.type === 'error') {
|
||||
streamingData?.closeStream()
|
||||
}
|
||||
}
|
||||
const streamingData = hooks?.useLogsStreaming([logBaseKey], onMessageStreaming, onError)
|
||||
|
||||
if (
|
||||
selectedItemData?.status === StepStatus.RUNNING &&
|
||||
status !== capitalizeFirstLetter(StepStatus.SUCCESS) &&
|
||||
!isEmpty(logContent.streamData)
|
||||
) {
|
||||
return streamingData.streamData[logBaseKey]
|
||||
}
|
||||
return logContent.blobDataCur
|
||||
}
|
||||
// State to manage expanded states of all containers
|
||||
const [expandedStates, setExpandedStates] = useState(new Map<string, boolean>())
|
||||
// UseEffect to initialize the states
|
||||
useEffect(() => {
|
||||
const states = new Map<string, boolean>()
|
||||
stepNameLogKeyMap?.forEach(value => {
|
||||
states.set(value.logBaseKey, false)
|
||||
})
|
||||
setExpandedStates(states) // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedItemData])
|
||||
const [isBottom, setIsBottom] = useState(false)
|
||||
const [expandedStates, setExpandedStates] = useState<Map<string, { expanded: boolean; streaming: boolean }>>(
|
||||
new Map()
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const states = new Map<string, { expanded: boolean; streaming: boolean }>()
|
||||
if (expandedStates.size === 0) {
|
||||
stepNameLogKeyMap?.forEach(_ => {
|
||||
states.set(_.logBaseKey, { expanded: false, streaming: false })
|
||||
})
|
||||
setExpandedStates(states)
|
||||
} // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [stepNameLogKeyMap])
|
||||
|
||||
const handleClick = () => {
|
||||
const logContainer = containerRef.current as HTMLDivElement
|
||||
const scrollParent = logContainer?.parentElement as HTMLDivElement
|
||||
if (!isBottom) {
|
||||
scrollParent.scrollTop = scrollParent.scrollHeight
|
||||
setIsBottom(true)
|
||||
} else if (isBottom) {
|
||||
scrollParent.scrollTop = 0
|
||||
setIsBottom(false)
|
||||
}
|
||||
}
|
||||
// Function to toggle expanded state of a container
|
||||
const toggleExpandedState = (key: string) => {
|
||||
const toggleExpandedState = useCallback((key: string) => {
|
||||
setExpandedStates(prevStates => {
|
||||
const newStates = new Map(prevStates)
|
||||
newStates.set(key, !newStates.get(key))
|
||||
const currentState = newStates.get(key) || { expanded: false, streaming: false }
|
||||
newStates.set(key, { ...currentState, expanded: !currentState.expanded })
|
||||
return newStates
|
||||
})
|
||||
}, [])
|
||||
|
||||
const [steps, setSteps] = useState(new Map())
|
||||
|
||||
useEffect(() => {
|
||||
if (stepNameLogKeyMap) {
|
||||
const newStatuses = new Map()
|
||||
stepNameLogKeyMap.forEach((value, key) => {
|
||||
newStatuses.set(key, value)
|
||||
})
|
||||
setSteps(newStatuses)
|
||||
}
|
||||
if (
|
||||
stepNameLogKeyMap &&
|
||||
(selectedItemData?.payload?.kind as EnumCheckPayloadKindExtended) === 'harness_stage' &&
|
||||
selectedItemData?.status !== 'running'
|
||||
) {
|
||||
const renderedSteps = Array.from(stepNameLogKeyMap?.entries() || []).map(([key, data], idx) => {
|
||||
}, [stepNameLogKeyMap, selectedItemData?.status, toggleExpandedState])
|
||||
|
||||
const renderedSteps = useMemo(() => {
|
||||
return Array.from(steps?.entries() || []).map(([key, data], idx) => {
|
||||
if (key === undefined || idx === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const expanded =
|
||||
expandedStates.get(data.logBaseKey)?.expanded || data.status === 'AsyncWaiting' || data.status === 'Queued'
|
||||
return (
|
||||
<Container key={data.logBaseKey} className={cx(css.pipelineSteps)}>
|
||||
<Container ref={containerRef} key={data.logBaseKey} className={cx(css.pipelineSteps)}>
|
||||
<Container className={css.stepContainer}>
|
||||
<Layout.Horizontal
|
||||
spacing="small"
|
||||
className={cx(css.stepHeader, {
|
||||
[css.expanded]: expandedStates.get(key),
|
||||
[css.selected]: expandedStates.get(key)
|
||||
[css.expanded]: expanded,
|
||||
[css.selected]: expanded
|
||||
})}
|
||||
{...ButtonRoleProps}
|
||||
onClick={() => {
|
||||
toggleExpandedState(key)
|
||||
toggleExpandedState(data.logBaseKey)
|
||||
}}>
|
||||
<NavArrowRight color={Utils.getRealCSSColor(Color.GREY_500)} className={cx(css.noShrink, css.chevron)} />
|
||||
<ExecutionStatus
|
||||
@ -146,28 +198,58 @@ const LogTerminal: React.FC<LogViewerProps> = ({
|
||||
</Text>
|
||||
</Render>
|
||||
</Layout.Horizontal>
|
||||
<Render when={expandedStates.get(key)}>
|
||||
<Render when={expanded}>
|
||||
<LogStageContainer
|
||||
value={key}
|
||||
status={data.status}
|
||||
getLogData={getLogData}
|
||||
logKey={data.logBaseKey}
|
||||
expanded={expandedStates.get(key)}
|
||||
expanded={expanded}
|
||||
setSelectedStage={setSelectedStage}
|
||||
/>
|
||||
<Button
|
||||
size={ButtonSize.SMALL}
|
||||
variation={ButtonVariation.PRIMARY}
|
||||
text={isBottom ? getString('top') : getString('bottom')}
|
||||
icon={isBottom ? 'arrow-up' : 'arrow-down'}
|
||||
iconProps={{ size: 10 }}
|
||||
onClick={handleClick}
|
||||
className={css.scrollDownBtn}
|
||||
/>
|
||||
</Render>
|
||||
</Container>
|
||||
</Container>
|
||||
)
|
||||
})
|
||||
return <>{renderedSteps}</>
|
||||
}
|
||||
return <Container key={`nolog_${containerKey}`} ref={ref} className={cx(css.main, className)} />
|
||||
}) // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [steps, selectedItemData, toggleExpandedState, expandedStates])
|
||||
return (
|
||||
<>
|
||||
{steps && (selectedItemData?.payload?.kind as EnumCheckPayloadKindExtended) === 'harness_stage' ? (
|
||||
renderedSteps
|
||||
) : (
|
||||
<Container key={`nolog_${containerKey}`} ref={ref} className={cx(css.main, className)} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const lineElement = (line = '') => {
|
||||
const element = document.createElement('pre')
|
||||
element.className = css.line
|
||||
element.innerHTML = Anser.ansiToHtml(line.replace(/\r?\n$/, ''))
|
||||
|
||||
// Function to escape special HTML characters
|
||||
const escapeHtml = (unsafe: string) => {
|
||||
return unsafe
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
// Escaping HTML special characters in the line
|
||||
const escapedLine = escapeHtml(line.replace(/\r?\n$/, ''))
|
||||
element.innerHTML = Anser.ansiToHtml(escapedLine)
|
||||
return element
|
||||
}
|
||||
|
||||
@ -175,9 +257,11 @@ export const LogViewer = React.memo(LogTerminal)
|
||||
|
||||
export interface LogStageContainerProps {
|
||||
stepNameLogKeyMap?: Map<string, string>
|
||||
expanded?: boolean // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getLogData: (logKey: string) => any
|
||||
expanded?: boolean
|
||||
|
||||
getLogData: (logKey: string, status: string, onMessageStreaming: (e: any) => void) => any
|
||||
logKey: string
|
||||
status: string
|
||||
value: string
|
||||
setSelectedStage: React.Dispatch<React.SetStateAction<TypesStage | null>>
|
||||
}
|
||||
@ -187,12 +271,50 @@ export const LogStageContainer: React.FC<LogStageContainerProps> = ({
|
||||
expanded,
|
||||
logKey,
|
||||
value,
|
||||
status,
|
||||
setSelectedStage
|
||||
}) => {
|
||||
const localRef = useRef<HTMLDivElement | null>(null) // Create a unique ref for each LogStageContainer instance
|
||||
const data = getLogData(logKey)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const onMessageStreaming = (e: any) => {
|
||||
if (e.data) {
|
||||
sendStreamLogToRenderer(e.data || '')
|
||||
}
|
||||
}
|
||||
const data = getLogData(logKey, status, onMessageStreaming)
|
||||
const sendStreamLogToRenderer = useScheduleJob({
|
||||
handler: useCallback((blocks: string[]) => {
|
||||
const logContainer = localRef.current as HTMLDivElement
|
||||
|
||||
if (logContainer) {
|
||||
const fragment = new DocumentFragment()
|
||||
|
||||
blocks.forEach((block: string) => {
|
||||
const blockData = JSON.parse(block)
|
||||
const linePos = blockData.pos + 1
|
||||
const localDate = new Date(blockData.time)
|
||||
const formattedDate = localDate.toLocaleString()
|
||||
|
||||
fragment.appendChild(
|
||||
lineElement(`${linePos} ${blockData.level} ${formattedDate.replace(',', '')} ${blockData.out}`)
|
||||
)
|
||||
})
|
||||
|
||||
const scrollParent = logContainer.parentElement?.parentElement?.parentElement as HTMLDivElement
|
||||
const autoScroll =
|
||||
scrollParent && scrollParent.scrollTop === scrollParent.scrollHeight - scrollParent.offsetHeight
|
||||
|
||||
logContainer?.appendChild(fragment)
|
||||
|
||||
if (autoScroll || scrollParent.scrollTop === 0) {
|
||||
scrollParent.scrollTop = scrollParent.scrollHeight
|
||||
}
|
||||
}
|
||||
}, []),
|
||||
isStreaming: true
|
||||
})
|
||||
useEffect(() => {
|
||||
if (expanded) {
|
||||
if (expanded && status !== StepStatus.RUNNING) {
|
||||
const pipelineArr = parseLogString(data)
|
||||
const fragment = new DocumentFragment()
|
||||
// Clear the container first
|
||||
@ -212,7 +334,9 @@ export const LogStageContainer: React.FC<LogStageContainerProps> = ({
|
||||
const logContainer = localRef.current as HTMLDivElement
|
||||
logContainer.appendChild(fragment)
|
||||
}
|
||||
}
|
||||
} else if (status === StepStatus.RUNNING) {
|
||||
sendStreamLogToRenderer(data || '')
|
||||
} // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [expanded, data, setSelectedStage])
|
||||
return <Container key={`harnesslog_${value}`} ref={localRef} className={cx(css.main, css.stepLogContainer)} />
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ export interface StringsMap {
|
||||
blameCommitLine: string
|
||||
blameEmpty: string
|
||||
botAlerts: string
|
||||
bottom: string
|
||||
branch: string
|
||||
branchCreated: string
|
||||
branchDeleted: string
|
||||
@ -205,6 +206,11 @@ export interface StringsMap {
|
||||
createWebhook: string
|
||||
created: string
|
||||
creationDate: string
|
||||
customDay: string
|
||||
customHour: string
|
||||
customMin: string
|
||||
customSecond: string
|
||||
customTime: string
|
||||
dangerDeleteRepo: string
|
||||
defaultBranch: string
|
||||
defaultBranchTitle: string
|
||||
@ -798,6 +804,7 @@ export interface StringsMap {
|
||||
title: string
|
||||
token: string
|
||||
tooltipRepoEdit: string
|
||||
top: string
|
||||
'triggers.actions': string
|
||||
'triggers.createSuccess': string
|
||||
'triggers.createTrigger': string
|
||||
|
@ -967,3 +967,10 @@ clear: Clear
|
||||
searchExamples: Search Examples
|
||||
nMoreMatches: This file contains {{ n }} more matches not shown.
|
||||
seeNMoreMatches: See all {{ n }} matches in the full file
|
||||
bottom: Bottom
|
||||
top: Top
|
||||
customTime: '{{days}} {{hours}} {{minutes}} {{seconds}}'
|
||||
customDay: '{{days}}d'
|
||||
customHour: '{{hours}}h'
|
||||
customMin: '{{minutes}}m'
|
||||
customSecond: '{{seconds}}s'
|
||||
|
@ -237,7 +237,7 @@ const CheckPipelineStep: React.FC<CheckPipelineStepsProps & { step: TypesStep }>
|
||||
)
|
||||
}
|
||||
|
||||
const createLogLineElement = (line = '') => {
|
||||
export const createLogLineElement = (line = '') => {
|
||||
const element = document.createElement('pre') as HTMLPreElement & {
|
||||
setHTML: (html: string, options: Record<string, unknown>) => void
|
||||
}
|
||||
|
@ -106,16 +106,29 @@ export const Checks: React.FC<ChecksProps> = ({ repoMetadata, pullReqMetadata, p
|
||||
if (queue.length !== 0) {
|
||||
processExecutionData(queue)
|
||||
} // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [queue, selectedStageId])
|
||||
|
||||
if (!prChecksDecisionResult) {
|
||||
return null
|
||||
}
|
||||
|
||||
}, [queue, selectedStageId, hookData])
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const enqueue = (item: any) => {
|
||||
setQueue(prevQueue => [...prevQueue, item])
|
||||
}
|
||||
useEffect(() => {
|
||||
const pollingInterval = 4000
|
||||
|
||||
const fetchAndProcessData = () => {
|
||||
hookData?.refetch()
|
||||
if (hookData && rootNodeId) {
|
||||
enqueue(rootNodeId) // Use your existing enqueue logic here
|
||||
}
|
||||
}
|
||||
// Set up the polling with setInterval
|
||||
const intervalId = setInterval(fetchAndProcessData, pollingInterval)
|
||||
// Clean up the interval on component unmount
|
||||
return () => clearInterval(intervalId)
|
||||
}, [hookData, enqueue, dispatch])
|
||||
|
||||
if (!prChecksDecisionResult) {
|
||||
return null
|
||||
}
|
||||
|
||||
const processExecutionData = (curQueue: string[]) => {
|
||||
const newQueue = [...curQueue]
|
||||
|
@ -21,6 +21,7 @@ import { get, sortBy } from 'lodash-es'
|
||||
import cx from 'classnames'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { Container, Layout, Text, FlexExpander, Utils } from '@harnessio/uicore'
|
||||
import ReactTimeago from 'react-timeago'
|
||||
import { Color, FontVariation } from '@harnessio/design-system'
|
||||
import {
|
||||
ButtonRoleProps,
|
||||
@ -30,6 +31,7 @@ import {
|
||||
timeDistance
|
||||
} from 'utils/Utils'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import { useQueryParams } from 'hooks/useQueryParams'
|
||||
import type { EnumCheckPayloadKind, TypesCheck, TypesStage } from 'services/code'
|
||||
import { ExecutionState, ExecutionStatus } from 'components/ExecutionStatus/ExecutionStatus'
|
||||
@ -44,7 +46,7 @@ interface ChecksMenuProps extends ChecksProps {
|
||||
|
||||
type TypesCheckPayloadExtended = EnumCheckPayloadKind | 'harness_stage'
|
||||
type ExpandedStates = { [key: string]: boolean }
|
||||
type ElapsedTimeStatusMap = { [key: string]: { status: 'string'; time: string } }
|
||||
type ElapsedTimeStatusMap = { [key: string]: { status: 'string'; time: string; started: string } }
|
||||
|
||||
enum CheckKindPayload {
|
||||
HARNESS_STAGE = 'harness_stage'
|
||||
@ -56,6 +58,7 @@ export const ChecksMenu: React.FC<ChecksMenuProps> = ({
|
||||
onDataItemChanged,
|
||||
setSelectedStage: setSelectedStageFromProps
|
||||
}) => {
|
||||
const { getString } = useStrings()
|
||||
const { routes, standalone } = useAppContext()
|
||||
const history = useHistory()
|
||||
const { uid } = useQueryParams<{ uid: string }>()
|
||||
@ -103,7 +106,9 @@ export const ChecksMenu: React.FC<ChecksMenuProps> = ({
|
||||
selectedStage
|
||||
])
|
||||
const [expandedPipelineId, setExpandedPipelineId] = useState<string | null>(null)
|
||||
const [statusTimeStates, setStatusTimeStates] = useState<{ [key: string]: { status: string; time: string } }>({})
|
||||
const [statusTimeStates, setStatusTimeStates] = useState<{
|
||||
[key: string]: { status: string; time: string; started: string }
|
||||
}>({})
|
||||
|
||||
const groupByPipeline = (data: TypesCheck[]) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ -159,7 +164,11 @@ export const ChecksMenu: React.FC<ChecksMenuProps> = ({
|
||||
})
|
||||
const res = findStatus()
|
||||
const statusVal = res ? res.status : ''
|
||||
initialMap[key] = { status: statusVal, time: timeDistance(startTime, endTime) }
|
||||
initialMap[key] = {
|
||||
status: statusVal,
|
||||
time: timeDistance(startTime, endTime),
|
||||
started: groupedData[key][0].started
|
||||
}
|
||||
}
|
||||
if (uid) {
|
||||
if (uid.includes(key)) {
|
||||
@ -173,6 +182,24 @@ export const ChecksMenu: React.FC<ChecksMenuProps> = ({
|
||||
})
|
||||
setStatusTimeStates(initialMap)
|
||||
}, [groupedData, uid])
|
||||
|
||||
const customFormatter = (_value: number, _unit: string, _suffix: string, date: Date | string | number) => {
|
||||
const now = new Date()
|
||||
const then = new Date(date)
|
||||
const secondsPast = (now.getTime() - then.getTime()) / 1000
|
||||
const days = Math.round(secondsPast / 86400)
|
||||
const remainder = secondsPast % 86400
|
||||
const hours = Math.floor(remainder / 3600)
|
||||
const minutes = Math.floor((remainder % 3600) / 60)
|
||||
const seconds = Math.floor(remainder % 60)
|
||||
|
||||
return getString('customTime', {
|
||||
days: days ? getString('customDay', { days }) : '',
|
||||
hours: hours ? getString('customHour', { hours }) : '',
|
||||
minutes: minutes ? getString('customMin', { minutes }) : '',
|
||||
seconds: seconds ? getString('customSecond', { seconds }) : ''
|
||||
})
|
||||
}
|
||||
return (
|
||||
<Layout.Vertical padding={{ top: 'large' }} spacing={'small'} className={cx(css.menu, css.leftPaneContent)}>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
|
||||
@ -209,7 +236,14 @@ export const ChecksMenu: React.FC<ChecksMenuProps> = ({
|
||||
padding={{ right: 'small' }}
|
||||
font={{ variation: FontVariation.SMALL }}
|
||||
className={css.noShrink}>
|
||||
{statusTimeStates[pipelineId]?.time}
|
||||
{statusTimeStates[pipelineId]?.status === 'running' ? (
|
||||
<ReactTimeago
|
||||
date={new Date(statusTimeStates[pipelineId]?.started || 0)}
|
||||
formatter={customFormatter}
|
||||
/>
|
||||
) : (
|
||||
statusTimeStates[pipelineId]?.time
|
||||
)}
|
||||
</Text>
|
||||
</Render>
|
||||
<NavArrowRight
|
||||
@ -227,6 +261,7 @@ export const ChecksMenu: React.FC<ChecksMenuProps> = ({
|
||||
prChecksDecisionResult={prChecksDecisionResult}
|
||||
key={itemData.uid}
|
||||
itemData={itemData}
|
||||
customFormatter={customFormatter}
|
||||
isPipeline={itemData.payload?.kind === PullRequestCheckType.PIPELINE}
|
||||
isSelected={itemData.uid === selectedUID}
|
||||
onClick={stage => {
|
||||
@ -261,6 +296,7 @@ interface CheckMenuItemProps extends ChecksProps {
|
||||
itemData: TypesCheck
|
||||
onClick: (stage?: TypesStage) => void
|
||||
setSelectedStage: (stage: TypesStage | null) => void
|
||||
customFormatter: (_value: number, _unit: string, _suffix: string, date: Date | string | number) => string
|
||||
}
|
||||
|
||||
const CheckMenuItem: React.FC<CheckMenuItemProps> = ({
|
||||
@ -270,7 +306,8 @@ const CheckMenuItem: React.FC<CheckMenuItemProps> = ({
|
||||
onClick,
|
||||
repoMetadata,
|
||||
pullReqMetadata,
|
||||
setSelectedStage
|
||||
setSelectedStage,
|
||||
customFormatter
|
||||
}) => {
|
||||
const [expanded, setExpanded] = useState(isSelected)
|
||||
|
||||
@ -317,7 +354,11 @@ const CheckMenuItem: React.FC<CheckMenuItemProps> = ({
|
||||
<FlexExpander />
|
||||
|
||||
<Text color={Color.GREY_300} font={{ variation: FontVariation.SMALL }} className={css.noShrink}>
|
||||
{timeDistance(itemData.updated, itemData.created)}
|
||||
{itemData?.ended && itemData?.started ? (
|
||||
timeDistance(itemData.updated, itemData.created)
|
||||
) : (
|
||||
<ReactTimeago date={new Date(itemData?.started || 0)} formatter={customFormatter} />
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<Render when={isPipeline}>
|
||||
|
@ -91,3 +91,7 @@ export function parseLogString(logString: string) {
|
||||
|
||||
return parsedLogs
|
||||
}
|
||||
|
||||
export function capitalizeFirstLetter(str: string) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user