mirror of
https://github.com/harness/drone.git
synced 2025-05-10 22:21:22 +08:00
feat: [code-1195]: integrate status check logs in harness mode (#922)
This commit is contained in:
parent
b873fb95ed
commit
35c815beb7
@ -33,3 +33,94 @@
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
}
|
||||
|
||||
.pipelineSteps {
|
||||
padding: 10px 20px 0 !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
height: 10px;
|
||||
width: 100%;
|
||||
background-color: var(--black);
|
||||
position: absolute;
|
||||
top: 64px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.stepContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.stepHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 34px;
|
||||
border-radius: 6px;
|
||||
padding: 0 10px 0 6px;
|
||||
position: sticky;
|
||||
top: 74px;
|
||||
z-index: 2;
|
||||
background-color: var(--black);
|
||||
|
||||
&.expanded {
|
||||
.chevron {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.chevron {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #22222aa9;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: #22222a;
|
||||
}
|
||||
|
||||
&.selected .name {
|
||||
color: var(--primary-7) !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: #b0b1c3 !important;
|
||||
font-weight: 400 !important;
|
||||
font-size: 14px !important;
|
||||
font-family: var(--font-family-mono);
|
||||
}
|
||||
}
|
||||
|
||||
.stepLogContainer {
|
||||
padding: 15px 10px 15px 36px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.consoleLine {
|
||||
color: var(--white);
|
||||
|
||||
@include mono-font;
|
||||
|
||||
word-wrap: break-word !important;
|
||||
white-space: pre-wrap !important;
|
||||
cursor: text;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
&:empty {
|
||||
display: inline-block;
|
||||
min-height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.noShrink {
|
||||
flex-shrink: inherit;
|
||||
}
|
||||
|
@ -16,5 +16,15 @@
|
||||
|
||||
/* eslint-disable */
|
||||
// This is an auto-generated file
|
||||
export declare const chevron: string
|
||||
export declare const consoleLine: string
|
||||
export declare const expanded: string
|
||||
export declare const line: string
|
||||
export declare const main: string
|
||||
export declare const name: string
|
||||
export declare const noShrink: string
|
||||
export declare const pipelineSteps: string
|
||||
export declare const selected: string
|
||||
export declare const stepContainer: string
|
||||
export declare const stepHeader: string
|
||||
export declare const stepLogContainer: string
|
||||
|
@ -14,26 +14,142 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import Anser from 'anser'
|
||||
import cx from 'classnames'
|
||||
import { Container } from '@harnessio/uicore'
|
||||
import { Container, FlexExpander, Layout, Text, Utils } from '@harnessio/uicore'
|
||||
import { NavArrowRight } from 'iconoir-react'
|
||||
import { Color } from '@harnessio/design-system'
|
||||
import { Render } from 'react-jsx-match'
|
||||
import { parseLogString } from 'pages/PullRequest/Checks/ChecksUtils'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { ButtonRoleProps } from 'utils/Utils'
|
||||
import type { EnumCheckPayloadKind, TypesCheck, TypesStage } from 'services/code'
|
||||
import css from './LogViewer.module.scss'
|
||||
|
||||
export interface LogViewerProps {
|
||||
search?: string
|
||||
content?: string
|
||||
className?: string
|
||||
stepNameLogKeyMap?: Map<string, string>
|
||||
setSelectedStage: React.Dispatch<React.SetStateAction<TypesStage | null>>
|
||||
selectedItemData: TypesCheck | undefined
|
||||
}
|
||||
|
||||
const LogTerminal: React.FC<LogViewerProps> = ({ content, className }) => {
|
||||
export interface LogLine {
|
||||
time: string
|
||||
message: string
|
||||
out: string
|
||||
level: string
|
||||
details: {
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
enum StepTypes {
|
||||
LITEENGINETASK = 'liteEngineTask',
|
||||
INITIALIZE = 'initialize'
|
||||
}
|
||||
|
||||
export type EnumCheckPayloadKindExtended = EnumCheckPayloadKind | 'harness_stage'
|
||||
|
||||
const LogTerminal: React.FC<LogViewerProps> = ({
|
||||
content,
|
||||
className,
|
||||
stepNameLogKeyMap,
|
||||
setSelectedStage,
|
||||
selectedItemData
|
||||
}) => {
|
||||
const { hooks } = useAppContext()
|
||||
const ref = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const containerKey = selectedItemData ? selectedItemData.id : 'default-key'
|
||||
useEffect(() => {
|
||||
content?.split(/\r?\n/).forEach(line => ref.current?.appendChild(lineElement(line)))
|
||||
}, [content])
|
||||
// Clear the container first
|
||||
if (ref.current) {
|
||||
ref.current.innerHTML = ''
|
||||
}
|
||||
|
||||
return <Container ref={ref} className={cx(css.main, className)} />
|
||||
if (stepNameLogKeyMap && content) {
|
||||
content.split(/\r?\n/).forEach(line => {
|
||||
if (ref.current) {
|
||||
ref.current.appendChild(lineElement(line))
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [content, stepNameLogKeyMap, selectedItemData])
|
||||
|
||||
const getLogData = (logBaseKey: string) => {
|
||||
const logContent = hooks?.useLogsContentHook([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, false)
|
||||
})
|
||||
setExpandedStates(states) // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
// Function to toggle expanded state of a container
|
||||
const toggleExpandedState = (key: string) => {
|
||||
setExpandedStates(prevStates => {
|
||||
const newStates = new Map(prevStates)
|
||||
newStates.set(key, !newStates.get(key))
|
||||
return newStates
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
stepNameLogKeyMap &&
|
||||
(selectedItemData?.payload?.kind as EnumCheckPayloadKindExtended) === 'harness_stage' &&
|
||||
selectedItemData?.status !== 'running'
|
||||
) {
|
||||
const renderedSteps = Array.from(stepNameLogKeyMap?.entries() || []).map(([key, value], idx) => {
|
||||
// const data = getLogData(value)
|
||||
if (key === undefined || idx === 0) {
|
||||
return
|
||||
}
|
||||
return (
|
||||
<Container key={value} 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)
|
||||
})}
|
||||
{...ButtonRoleProps}
|
||||
onClick={() => {
|
||||
toggleExpandedState(key)
|
||||
}}>
|
||||
<NavArrowRight color={Utils.getRealCSSColor(Color.GREY_500)} className={cx(css.noShrink, css.chevron)} />
|
||||
|
||||
<Text className={css.name} lineClamp={1}>
|
||||
{key === StepTypes.LITEENGINETASK ? StepTypes.INITIALIZE : key}
|
||||
</Text>
|
||||
|
||||
<FlexExpander />
|
||||
</Layout.Horizontal>
|
||||
<Render when={expandedStates.get(key)}>
|
||||
<LogStageContainer
|
||||
value={key}
|
||||
getLogData={getLogData}
|
||||
logKey={value}
|
||||
expanded={expandedStates.get(key)}
|
||||
setSelectedStage={setSelectedStage}
|
||||
/>
|
||||
</Render>
|
||||
</Container>
|
||||
</Container>
|
||||
)
|
||||
})
|
||||
return <>{renderedSteps}</>
|
||||
}
|
||||
return <Container key={`nolog_${containerKey}`} ref={ref} className={cx(css.main, className)} />
|
||||
}
|
||||
|
||||
const lineElement = (line = '') => {
|
||||
@ -44,3 +160,41 @@ const lineElement = (line = '') => {
|
||||
}
|
||||
|
||||
export const LogViewer = React.memo(LogTerminal)
|
||||
|
||||
export interface LogStageContainerProps {
|
||||
stepNameLogKeyMap?: Map<string, string>
|
||||
expanded?: boolean
|
||||
getLogData: (logKey: string) => any // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
logKey: string
|
||||
value: string
|
||||
setSelectedStage: React.Dispatch<React.SetStateAction<TypesStage | null>>
|
||||
}
|
||||
|
||||
export const LogStageContainer: React.FC<LogStageContainerProps> = ({
|
||||
getLogData,
|
||||
stepNameLogKeyMap,
|
||||
expanded,
|
||||
logKey,
|
||||
value,
|
||||
setSelectedStage
|
||||
}) => {
|
||||
const localRef = useRef<HTMLDivElement | null>(null) // Create a unique ref for each LogStageContainer instance
|
||||
const data = getLogData(logKey)
|
||||
|
||||
useEffect(() => {
|
||||
if (expanded) {
|
||||
const pipelineArr = parseLogString(data)
|
||||
const fragment = new DocumentFragment()
|
||||
// Clear the container first
|
||||
if (localRef.current) {
|
||||
localRef.current.innerHTML = ''
|
||||
}
|
||||
if (pipelineArr) {
|
||||
pipelineArr?.forEach((line: LogLine) => fragment.appendChild(lineElement(line.message)))
|
||||
const logContainer = localRef.current as HTMLDivElement
|
||||
logContainer.appendChild(fragment)
|
||||
}
|
||||
}
|
||||
}, [expanded, data, setSelectedStage, stepNameLogKeyMap])
|
||||
return <Container key={`harnesslog_${value}`} ref={localRef} className={cx(css.main, css.stepLogContainer)} />
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import React, { useEffect, useMemo, useReducer, useState } from 'react'
|
||||
import { Falsy, Match, Render, Truthy } from 'react-jsx-match'
|
||||
import { get } from 'lodash-es'
|
||||
import cx from 'classnames'
|
||||
@ -34,12 +34,32 @@ import { CheckPipelineSteps } from './CheckPipelineSteps'
|
||||
import { ChecksMenu } from './ChecksMenu'
|
||||
import css from './Checks.module.scss'
|
||||
|
||||
// Define the reducer outside your component
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const mapReducer = (state: any, action: { type: any; newEntries: any }) => {
|
||||
switch (action.type) {
|
||||
case 'UPDATE_MAP':
|
||||
// Return a new updated map
|
||||
return new Map([...state, ...action.newEntries])
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
interface SelectedItemDataInterface {
|
||||
execution_id: string
|
||||
stage_id: string
|
||||
}
|
||||
|
||||
export const Checks: React.FC<ChecksProps> = ({ repoMetadata, pullRequestMetadata, prChecksDecisionResult }) => {
|
||||
const { getString } = useStrings()
|
||||
const history = useHistory()
|
||||
const { routes } = useAppContext()
|
||||
const { routes, standalone } = useAppContext()
|
||||
const [selectedItemData, setSelectedItemData] = useState<TypesCheck>()
|
||||
const [selectedStage, setSelectedStage] = useState<TypesStage | null>(null)
|
||||
const [queue, setQueue] = useState<string[]>([])
|
||||
const [stepNameLogKeyMap, dispatch] = useReducer(mapReducer, new Map())
|
||||
const { hooks } = useAppContext()
|
||||
const isCheckDataMarkdown = useMemo(
|
||||
() => selectedItemData?.payload?.kind === PullRequestCheckType.MARKDOWN,
|
||||
[selectedItemData?.payload?.kind]
|
||||
@ -59,11 +79,78 @@ export const Checks: React.FC<ChecksProps> = ({ repoMetadata, pullRequestMetadat
|
||||
return selectedItemData?.link
|
||||
}
|
||||
}, [repoMetadata?.path, routes, selectedItemData, selectedStage])
|
||||
const executionId =
|
||||
standalone && selectedItemData ? null : (selectedItemData?.payload?.data as SelectedItemDataInterface)?.execution_id
|
||||
const selectedStageId =
|
||||
standalone && selectedItemData ? null : (selectedItemData?.payload?.data as SelectedItemDataInterface)?.stage_id
|
||||
|
||||
const hookData = hooks?.useExecutionDataHook?.(executionId, selectedStageId)
|
||||
const executionApiCallData = hookData?.data
|
||||
const rootNodeId = executionApiCallData?.data?.executionGraph?.rootNodeId
|
||||
|
||||
useEffect(() => {
|
||||
if (rootNodeId) {
|
||||
enqueue(rootNodeId)
|
||||
}
|
||||
}, [rootNodeId])
|
||||
|
||||
useEffect(() => {
|
||||
if (queue.length !== 0) {
|
||||
processExecutionData(queue)
|
||||
} // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [queue])
|
||||
|
||||
if (!prChecksDecisionResult) {
|
||||
return null
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const enqueue = (item: any) => {
|
||||
setQueue(prevQueue => [...prevQueue, item])
|
||||
}
|
||||
|
||||
const processExecutionData = (curQueue: string[]) => {
|
||||
const newQueue = [...curQueue]
|
||||
const newEntries = []
|
||||
|
||||
while (newQueue.length !== 0) {
|
||||
const item = newQueue.shift()
|
||||
if (item) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const nodeArray = (executionApiCallData.data.executionGraph.nodeAdjacencyListMap as any)[item]
|
||||
//add node to queue
|
||||
if (nodeArray) {
|
||||
nodeArray.children.map((node: string) => {
|
||||
newQueue.push(node)
|
||||
})
|
||||
}
|
||||
if (nodeArray) {
|
||||
nodeArray.nextIds.map((node: string) => {
|
||||
newQueue.push(node)
|
||||
})
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const nodeMapItem = (executionApiCallData.data.executionGraph.nodeMap as any)[item]
|
||||
if (nodeMapItem) {
|
||||
// Assume that you generate a key-yarn value pair for the map here
|
||||
const key = nodeMapItem.stepParameters.name
|
||||
const value = nodeMapItem.logBaseKey
|
||||
if (item !== rootNodeId) {
|
||||
newEntries.push([key, value])
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update the map using the reducer
|
||||
if (newEntries.length > 0) {
|
||||
dispatch({ type: 'UPDATE_MAP', newEntries })
|
||||
}
|
||||
|
||||
setQueue(newQueue) // Update the queue state
|
||||
}
|
||||
|
||||
return (
|
||||
<Container className={css.main}>
|
||||
<Match expr={prChecksDecisionResult?.overallStatus}>
|
||||
@ -141,7 +228,13 @@ export const Checks: React.FC<ChecksProps> = ({ repoMetadata, pullRequestMetadat
|
||||
/>
|
||||
</Truthy>
|
||||
<Falsy>
|
||||
<LogViewer content={logContent} className={css.logViewer} />
|
||||
<LogViewer
|
||||
content={logContent}
|
||||
stepNameLogKeyMap={stepNameLogKeyMap}
|
||||
className={css.logViewer}
|
||||
setSelectedStage={setSelectedStage}
|
||||
selectedItemData={selectedItemData}
|
||||
/>
|
||||
</Falsy>
|
||||
</Match>
|
||||
</Falsy>
|
||||
|
@ -35,3 +35,40 @@ export function findDefaultExecution<T>(collection: Iterable<T> | null | undefin
|
||||
(collection as CheckType)[0]) as T)
|
||||
: null
|
||||
}
|
||||
export interface DetailDict {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
export function parseLogString(logString: string) {
|
||||
if (!logString) {
|
||||
return ''
|
||||
}
|
||||
const logEntries = logString.trim().split('\n')
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const parsedLogs: any = []
|
||||
logEntries.forEach(entry => {
|
||||
// Parse the entry as JSON
|
||||
const jsonEntry = JSON.parse(entry)
|
||||
// Apply the regex to the 'out' field
|
||||
const parts = jsonEntry.out.match(/time="([^"]+)" level=([^ ]+) msg="([^"]+)"(.*)/)
|
||||
|
||||
if (parts) {
|
||||
const [, time, level, message, details, out] = parts
|
||||
const detailParts = details.trim().split(' ')
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const detailDict: any = {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
detailParts.forEach((part: any) => {
|
||||
if (part.includes('=')) {
|
||||
const [key, value] = part.split('=')
|
||||
detailDict[key.trim()] = value.trim()
|
||||
}
|
||||
})
|
||||
parsedLogs.push({ time, level, message, out, details: detailDict })
|
||||
} else {
|
||||
parsedLogs.push({ time: jsonEntry.time, level: jsonEntry.level, message: jsonEntry.out })
|
||||
}
|
||||
})
|
||||
|
||||
return parsedLogs
|
||||
}
|
||||
|
@ -63,6 +63,7 @@ import css from './PullRequestActionsBox.module.scss'
|
||||
|
||||
const codeOwnersNotFoundMessage = 'CODEOWNERS file not found'
|
||||
const codeOwnersNotFoundMessage2 = `path "CODEOWNERS" not found`
|
||||
const codeOwnersNotFoundMessage3 = `failed to find node 'CODEOWNERS' in 'main': failed to get tree node: failed to ls file: path "CODEOWNERS" not found`
|
||||
|
||||
const POLLING_INTERVAL = 60000
|
||||
export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
|
||||
@ -134,7 +135,8 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
|
||||
setAllowedStrats(err.allowed_methods)
|
||||
} else if (
|
||||
getErrorMessage(err) === codeOwnersNotFoundMessage ||
|
||||
getErrorMessage(err) === codeOwnersNotFoundMessage2
|
||||
getErrorMessage(err) === codeOwnersNotFoundMessage2 ||
|
||||
getErrorMessage(err) === codeOwnersNotFoundMessage3
|
||||
) {
|
||||
return
|
||||
} else {
|
||||
@ -147,8 +149,8 @@ export const PullRequestActionsBox: React.FC<PullRequestActionsBoxProps> = ({
|
||||
useEffect(() => {
|
||||
// recheck PR in case source SHA changed or PR was marked as unchecked
|
||||
// TODO: optimize call to handle all causes and avoid double calls by keeping track of SHA
|
||||
dryMerge()
|
||||
}, [unchecked, pullRequestMetadata?.source_sha]) // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
dryMerge() // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [unchecked, pullRequestMetadata?.source_sha])
|
||||
|
||||
useEffect(() => {
|
||||
// dryMerge()
|
||||
|
Loading…
Reference in New Issue
Block a user