pipeline console WIP

This commit is contained in:
Dan Wilson 2023-08-29 16:00:01 +01:00
parent 6ca2a6924d
commit 15a4fe04bd
16 changed files with 426 additions and 10 deletions

View File

@ -0,0 +1,32 @@
.container {
display: flex;
flex-direction: column;
background-color: black;
height: 100%;
overflow-y: auto;
}
.log {
color: white;
font-family: Inconsolata, monospace;
font-size: 2rem;
}
.header {
position: sticky;
top: 0;
background-color: var(--black);
height: var(--log-content-header-height);
.headerLayout {
display: flex;
align-items: baseline;
border-bottom: 1px solid var(--grey-800);
padding: var(--spacing-medium) 0;
font-weight: 600;
}
}
.steps {
padding: var(--spacing-medium) !important;
}

View File

@ -0,0 +1,10 @@
/* eslint-disable */
// this is an auto-generated file
declare const styles: {
readonly container: string
readonly log: string
readonly header: string
readonly headerLayout: string
readonly steps: string
}
export default styles

View File

@ -0,0 +1,48 @@
import React, { FC } from 'react'
import { useParams } from 'react-router-dom'
import { Container, Layout, Text } from '@harnessio/uicore'
import { Color, FontVariation } from '@harnessio/design-system'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import type { CODEProps } from 'RouteDefinitions'
import type { TypesStage } from 'services/code'
import ConsoleStep from 'components/ConsoleStep/ConsoleStep'
import css from './Console.module.scss'
interface ConsoleProps {
stage: TypesStage | undefined
}
const Console: FC<ConsoleProps> = ({ stage }) => {
const space = useGetSpaceParam()
const { pipeline, execution: executionNum } = useParams<CODEProps>()
return (
<div className={css.container}>
<Container className={css.header}>
<Layout.Horizontal className={css.headerLayout} spacing="small">
<Text font={{ variation: FontVariation.H4 }} color={Color.WHITE} padding={{ left: 'large', right: 'large' }}>
{stage?.name}
</Text>
<Text font={{ variation: FontVariation.BODY }} color={Color.GREY_500}>
{/* this needs fixed */}
Success in 5 mins
</Text>
</Layout.Horizontal>
</Container>
<Layout.Vertical className={css.steps} spacing="small">
{stage?.steps?.map((step, index) => (
<ConsoleStep
key={index}
step={step}
executionNumber={Number(executionNum)}
pipelineName={pipeline}
spaceName={space}
stageNumber={stage.number}
/>
))}
</Layout.Vertical>
</div>
)
}
export default Console

View File

@ -0,0 +1,14 @@
.logLayout {
margin-left: 2.3rem !important;
}
.lineNumber {
width: 1.5rem;
color: #999;
margin-right: 1rem;
}
.log {
color: white !important;
margin-bottom: 1rem;
}

View File

@ -0,0 +1,8 @@
/* eslint-disable */
// this is an auto-generated file
declare const styles: {
readonly logLayout: string
readonly lineNumber: string
readonly log: string
}
export default styles

View File

@ -0,0 +1,36 @@
import { Layout, Text } from '@harnessio/uicore'
import React, { FC } from 'react'
import css from './ConsoleLogs.module.scss'
// currently a string - should be an array of strings in future
interface ConsoleLogsProps {
logs: string
}
interface log {
pos: number
out: string
time: number
}
const convertStringToLogArray = (logs: string): log[] => {
const logStrings = logs.split('\n').map(log => {
return JSON.parse(log)
})
return logStrings
}
const ConsoleLogs: FC<ConsoleLogsProps> = ({ logs }) => {
const logArray = convertStringToLogArray(logs)
return logArray.map((log, index) => {
return (
<Layout.Horizontal key={index} spacing={'medium'} className={css.logLayout}>
<Text className={css.lineNumber}>{log.pos}</Text>
<Text className={css.log}>{log.out}</Text>
</Layout.Horizontal>
)
})
}
export default ConsoleLogs

View File

@ -0,0 +1,5 @@
.stepLayout {
display: flex;
align-items: center;
cursor: pointer;
}

View File

@ -0,0 +1,6 @@
/* eslint-disable */
// this is an auto-generated file
declare const styles: {
readonly stepLayout: string
}
export default styles

View File

@ -0,0 +1,64 @@
import { Icon } from '@harnessio/icons'
import { FlexExpander, Layout } from '@harnessio/uicore'
import React, { FC, useEffect } from 'react'
import { useGet } from 'restful-react'
import { Text } from '@harnessio/uicore'
import type { TypesStep } from 'services/code'
import { timeDistance } from 'utils/Utils'
import ConsoleLogs from 'components/ConsoleLogs/ConsoleLogs'
import css from './ConsoleStep.module.scss'
interface ConsoleStepProps {
step: TypesStep | undefined
stageNumber: number | undefined
spaceName: string
pipelineName: string | undefined
executionNumber: number
}
const ConsoleStep: FC<ConsoleStepProps> = ({ step, stageNumber, spaceName, pipelineName, executionNumber }) => {
const [isOpened, setIsOpened] = React.useState(false)
const { data, error, loading, refetch } = useGet<string>({
path: `/api/v1/pipelines/${spaceName}/${pipelineName}/+/executions/${executionNumber}/logs/${String(
stageNumber
)}/${String(step?.number)}`,
lazy: true
})
// this refetches any open steps when the stage number changes - really it shouldnt refetch until reopened...
useEffect(() => {
setIsOpened(false)
refetch()
}, [stageNumber, refetch])
return (
<>
<Layout.Horizontal
className={css.stepLayout}
spacing="medium"
onClick={() => {
setIsOpened(!isOpened)
if (!data && !loading) refetch()
}}>
<Icon name={isOpened ? 'chevron-down' : 'chevron-right'} />
<Icon name={step?.status === 'Success' ? 'success-tick' : 'circle'} />
<Text>{step?.name}</Text>
<FlexExpander />
{step?.started && step?.stopped && <div>{timeDistance(step?.stopped, step?.started)}</div>}
</Layout.Horizontal>
{isOpened ? (
loading ? (
<div>Loading...</div>
) : error ? (
<div>Error: {error}</div>
) : data ? (
<ConsoleLogs logs={data} />
) : null
) : null}
</>
)
}
export default ConsoleStep

View File

@ -0,0 +1,39 @@
.container {
display: flex;
flex-direction: column;
height: 100%;
}
.menu {
--stage-title-height: 54px;
width: 100%;
height: 100%;
.menuItem {
margin: 0.5rem 0 0.5rem 1rem !important;
cursor: pointer;
.layout {
display: flex;
align-items: center;
min-height: var(--stage-title-height);
padding: 0 var(--spacing-medium) 0 var(--spacing-medium);
border-radius: 10px 0 0 10px !important;
&.selected {
background-color: var(--primary-1);
.uid {
color: var(--primary-7) !important;
}
}
.uid {
color: var(--grey-700) !important;
font-weight: 600 !important;
font-size: 1rem !important;
}
}
}
}

View File

@ -0,0 +1,11 @@
/* eslint-disable */
// this is an auto-generated file
declare const styles: {
readonly container: string
readonly menu: string
readonly menuItem: string
readonly layout: string
readonly selected: string
readonly uid: string
}
export default styles

View File

@ -0,0 +1,56 @@
import React, { FC } from 'react'
import { Container, Layout, Text } from '@harnessio/uicore'
import { Icon } from '@harnessio/icons'
import cx from 'classnames'
import type { TypesStage } from 'services/code'
import css from './ExecutionStageList.module.scss'
interface ExecutionStageListProps {
stages: TypesStage[]
selectedStage: number | null
setSelectedStage: (selectedStep: number | null) => void
}
interface ExecutionStageProps {
stage: TypesStage
isSelected?: boolean
selectedStage: number | null
setSelectedStage: (selectedStage: number | null) => void
}
const ExecutionStage: FC<ExecutionStageProps> = ({ stage, isSelected = false, setSelectedStage = () => {} }) => {
return (
<Container
className={css.menuItem}
onClick={() => {
setSelectedStage(stage.number || null)
}}>
<Layout.Horizontal spacing="small" className={cx(css.layout, { [css.selected]: isSelected })}>
<Icon name="success-tick" size={16} />
<Text className={css.uid} lineClamp={1}>
{stage.name}
</Text>
</Layout.Horizontal>
</Container>
)
}
const ExecutionStageList: FC<ExecutionStageListProps> = ({ stages, setSelectedStage, selectedStage }) => {
return (
<Container className={css.menu}>
{stages.map((stage, index) => {
return (
<ExecutionStage
key={index}
stage={stage}
isSelected={selectedStage === stage.number}
selectedStage={selectedStage}
setSelectedStage={setSelectedStage}
/>
)
})}
</Container>
)
}
export default ExecutionStageList

View File

@ -1,4 +1,59 @@
.main {
min-height: var(--page-height);
background-color: var(--primary-bg) !important;
:global {
.Resizer {
background-color: var(--grey-300);
opacity: 0.2;
z-index: 1;
box-sizing: border-box;
background-clip: padding-box;
}
.Resizer:hover {
transition: all 2s ease;
}
.Resizer.horizontal {
margin: -5px 0;
border-top: 5px solid rgba(255, 255, 255, 0);
border-bottom: 5px solid rgba(255, 255, 255, 0);
cursor: row-resize;
}
.Resizer.horizontal:hover {
border-top: 5px solid rgba(0, 0, 0, 0.5);
border-bottom: 5px solid rgba(0, 0, 0, 0.5);
}
.Resizer.vertical {
width: 11px;
margin: 0 -5px;
border-left: 5px solid rgba(255, 255, 255, 0);
border-right: 5px solid rgba(255, 255, 255, 0);
cursor: col-resize;
}
.Resizer.vertical:hover {
border-left: 5px solid rgba(0, 0, 0, 0.5);
border-right: 5px solid rgba(0, 0, 0, 0.5);
}
.Resizer.disabled {
cursor: not-allowed;
}
.Resizer.disabled:hover {
border-color: transparent;
}
}
}
.container {
height: calc(100vh - var(--page-header-height));
}
.withError {
display: grid;
}

View File

@ -2,5 +2,7 @@
// this is an auto-generated file
declare const styles: {
readonly main: string
readonly container: string
readonly withError: string
}
export default styles

View File

@ -1,29 +1,59 @@
import React from 'react'
import { Container, PageHeader } from '@harnessio/uicore'
import { Container, PageHeader, PageBody } from '@harnessio/uicore'
import React, { useState } from 'react'
import cx from 'classnames'
import { useParams } from 'react-router-dom'
import { useGet } from 'restful-react'
import SplitPane from 'react-split-pane'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import type { CODEProps } from 'RouteDefinitions'
import type { TypesExecution } from 'services/code'
import ExecutionStageList from 'components/ExecutionStageList/ExecutionStageList'
import Console from 'components/Console/Console'
import { getErrorMessage, voidFn } from 'utils/Utils'
import { useStrings } from 'framework/strings'
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
import noExecutionImage from '../RepositoriesListing/no-repo.svg'
import css from './Execution.module.scss'
const Execution = () => {
const space = useGetSpaceParam()
const { pipeline, execution: executionNum } = useParams<CODEProps>()
const { getString } = useStrings()
const {
data: execution
// error,
// loading,
// refetch
// response
data: execution,
error,
loading,
refetch
} = useGet<TypesExecution>({
path: `/api/v1/pipelines/${space}/${pipeline}/+/executions/${executionNum}`
})
const [selectedStage, setSelectedStage] = useState<number | null>(null)
return (
<Container className={css.main}>
<PageHeader title={`EXECUTION STATUS = ${execution?.status}`} />
<PageHeader title={execution?.title} />
<PageBody
className={cx({ [css.withError]: !!error })}
error={error ? getErrorMessage(error) : null}
retryOnError={voidFn(refetch)}
noData={{
when: () => !execution,
image: noExecutionImage,
message: getString('executions.noData')
// button: NewExecutionButton
}}>
<LoadingSpinner visible={loading} />
<SplitPane split="vertical" size={300} minSize={200} maxSize={400}>
<ExecutionStageList
stages={execution?.stages || []}
setSelectedStage={setSelectedStage}
selectedStage={selectedStage}
/>
{selectedStage && <Console stage={execution?.stages?.[selectedStage - 1]} />}
</SplitPane>
</PageBody>
</Container>
)
}

View File

@ -61,7 +61,7 @@ const ExecutionList = () => {
const columns: Column<TypesExecution>[] = useMemo(
() => [
{
Header: getString('repos.name'),
Header: getString('executions.name'),
width: 'calc(100% - 180px)',
Cell: ({ row }: CellProps<TypesExecution>) => {
const record = row.original
@ -127,7 +127,7 @@ const ExecutionList = () => {
routes.toCODEExecution({
space,
pipeline: pipeline as string,
execution: String(executionInfo.id)
execution: String(executionInfo.number)
})
)
}