diff --git a/web/src/components/ExecutionStatus/ExecutionStatus.tsx b/web/src/components/ExecutionStatus/ExecutionStatus.tsx index eab6251cb..a26da0cf0 100644 --- a/web/src/components/ExecutionStatus/ExecutionStatus.tsx +++ b/web/src/components/ExecutionStatus/ExecutionStatus.tsx @@ -43,7 +43,8 @@ interface ExecutionStatusProps { export enum ExecutionStateExtended { FAILED = 'failed', - ABORTED = 'aborted' + ABORTED = 'aborted', + ASYNCWAITING = 'asyncwaiting' } export const ExecutionStatus: React.FC = ({ @@ -102,6 +103,11 @@ export const ExecutionStatus: React.FC = ({ icon: 'execution-stopped', css: null, title: getString('killed').toLocaleUpperCase() + }, + [ExecutionStateExtended.ASYNCWAITING]: { + icon: 'running-filled', + css: css.running, + title: getString('running').toLocaleUpperCase() } }), [getString, inExecution, isCi] diff --git a/web/src/components/LogViewer/LogViewer.module.scss b/web/src/components/LogViewer/LogViewer.module.scss index 0b5ad8248..c7cf7f059 100644 --- a/web/src/components/LogViewer/LogViewer.module.scss +++ b/web/src/components/LogViewer/LogViewer.module.scss @@ -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; + } +} diff --git a/web/src/components/LogViewer/LogViewer.module.scss.d.ts b/web/src/components/LogViewer/LogViewer.module.scss.d.ts index 044fd1733..520c90152 100644 --- a/web/src/components/LogViewer/LogViewer.module.scss.d.ts +++ b/web/src/components/LogViewer/LogViewer.module.scss.d.ts @@ -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 diff --git a/web/src/components/LogViewer/LogViewer.tsx b/web/src/components/LogViewer/LogViewer.tsx index 75bcfb3ee..4653474f1 100644 --- a/web/src/components/LogViewer/LogViewer.tsx +++ b/web/src/components/LogViewer/LogViewer.tsx @@ -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 = ({ @@ -63,9 +70,10 @@ const LogTerminal: React.FC = ({ selectedItemData }) => { const { hooks } = useAppContext() + const { getString } = useStrings() const ref = useRef(null) - + const containerRef = useRef(null) const containerKey = selectedItemData ? selectedItemData.id : 'default-key' useEffect(() => { // Clear the container first @@ -82,50 +90,94 @@ const LogTerminal: React.FC = ({ } }, [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()) - // UseEffect to initialize the states - useEffect(() => { - const states = new Map() - 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>( + new Map() + ) + useEffect(() => { + const states = new Map() + 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 }) - } - if ( - stepNameLogKeyMap && - (selectedItemData?.payload?.kind as EnumCheckPayloadKindExtended) === 'harness_stage' && - selectedItemData?.status !== 'running' - ) { - const renderedSteps = Array.from(stepNameLogKeyMap?.entries() || []).map(([key, data], idx) => { + }, []) + + const [steps, setSteps] = useState(new Map()) + + useEffect(() => { + if (stepNameLogKeyMap) { + const newStatuses = new Map() + stepNameLogKeyMap.forEach((value, key) => { + newStatuses.set(key, value) + }) + setSteps(newStatuses) + } + }, [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 ( - + { - toggleExpandedState(key) + toggleExpandedState(data.logBaseKey) }}> = ({ - + +