small ui fixes in pipelines (#501)

This commit is contained in:
Dan Wilson 2023-09-15 11:21:22 +00:00 committed by Harness
parent 4ff032c968
commit a6dcc0a6ef
33 changed files with 322 additions and 190 deletions

View File

@ -34,7 +34,7 @@ const Console: FC<ConsoleProps> = ({ stage, repoPath }) => {
</Text>
{stage?.started && stage?.stopped && (
<Text font={{ variation: FontVariation.BODY }} color={Color.GREY_500}>
{getString('executions.completedTime', { timeString: timeDistance(stage?.stopped, Date.now()) })}
{getString('executions.completedTime', { timeString: timeDistance(stage?.stopped, Date.now(), true) })}
</Text>
)}
</Layout.Horizontal>

View File

@ -1,5 +1,5 @@
.logLayout {
margin-left: 30px !important;
margin-left: 23px !important;
}
.lineNumber {
@ -7,6 +7,8 @@
color: #999;
margin-right: 16px;
font-family: 'Roboto Mono' !important;
user-select: none !important;
text-align: right !important;
}
.log {
@ -14,3 +16,7 @@
margin-bottom: var(--spacing-medium);
font-family: 'Roboto Mono' !important;
}
.time {
user-select: none !important;
}

View File

@ -3,3 +3,4 @@
export declare const lineNumber: string
export declare const log: string
export declare const logLayout: string
export declare const time: string

View File

@ -13,10 +13,10 @@ const ConsoleLogs: FC<ConsoleLogsProps> = ({ logs }) => {
{logs.map((log, index) => {
return (
<Layout.Horizontal key={index} spacing={'medium'} className={css.logLayout}>
<Text className={css.lineNumber}>{log.pos}</Text>
{typeof log.pos === 'number' && <Text className={css.lineNumber}>{log.pos + 1}</Text>}
<Text className={css.log}>{log.out}</Text>
<FlexExpander />
<Text>{log.time}s</Text>
<Text className={css.time}>{log.time}s</Text>
</Layout.Horizontal>
)
})}

View File

@ -33,3 +33,7 @@
}
}
}
.time {
user-select: none !important;
}

View File

@ -3,4 +3,5 @@
export declare const loading: string
export declare const spin: string
export declare const stepLayout: string
export declare const time: string
export declare const timeoutIcon: string

View File

@ -58,10 +58,13 @@ const ConsoleStep: FC<ConsoleStepProps> = ({ step, stageNumber, repoPath, pipeli
}
}
return () => {
if (step?.status === ExecutionState.RUNNING && isOpened) {
refetch()
}
setStreamingLogs([])
if (eventSourceRef.current) eventSourceRef.current.close()
}
}, [executionNumber, pipelineName, repoPath, stageNumber, step?.number, step?.status])
}, [executionNumber, isOpened, pipelineName, repoPath, stageNumber, step?.name, step?.number, step?.status])
let icon
if (step?.status === ExecutionState.SUCCESS) {
@ -107,7 +110,9 @@ const ConsoleStep: FC<ConsoleStepProps> = ({ step, stageNumber, repoPath, pipeli
{icon}
<Text>{step?.name}</Text>
<FlexExpander />
{step?.started && step?.stopped && <div>{timeDistance(step?.stopped, step?.started)}</div>}
{step?.started && step?.stopped && (
<Text className={css.time}>{timeDistance(step?.stopped, step?.started, true)}</Text>
)}
</Layout.Horizontal>
{isOpened && content}

View File

@ -9,10 +9,12 @@ import { useAppContext } from 'AppContext'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import type { CODEProps } from 'RouteDefinitions'
import type { GitInfoProps } from 'utils/GitUtils'
import { ExecutionStatus } from 'components/ExecutionStatus/ExecutionStatus'
import { ExecutionState, ExecutionStatus } from 'components/ExecutionStatus/ExecutionStatus'
import { getStatus } from 'utils/ExecutionUtils'
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
import { timeDistance } from 'utils/Utils'
import { useLiveTimer } from 'hooks/useLiveTimeHook'
import { CommitActions } from 'components/CommitActions/CommitActions'
import css from './ExecutionPageHeader.module.scss'
interface BreadcrumbLink {
@ -48,6 +50,8 @@ export function ExecutionPageHeader({
const { getString } = useStrings()
const space = useGetSpaceParam()
const { routes } = useAppContext()
const isActive = executionInfo?.status === ExecutionState.RUNNING
const currentTime = useLiveTimer(true)
if (!repoMetadata) {
return null
@ -94,23 +98,38 @@ export function ExecutionPageHeader({
{executionInfo.source}
</Text>
<PipeSeparator height={7} />
<Link
to={routes.toCODECommit({ repoPath: repoMetadata.path as string, commitRef: executionInfo.hash })}
className={css.hash}>
{executionInfo.hash?.slice(0, 6)}
</Link>
{executionInfo.hash && (
<Container onClick={Utils.stopEvent}>
<CommitActions
href={routes.toCODECommit({
repoPath: repoMetadata.path as string,
commitRef: executionInfo.hash
})}
sha={executionInfo.hash}
enableCopy
/>
</Container>
)}
<FlexExpander />
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center' }} className={css.timer}>
<Timer height={16} width={16} color={Utils.getRealCSSColor(Color.GREY_500)} />
<Text inline color={Color.GREY_500} font={{ size: 'small' }}>
{timeDistance(executionInfo.started, executionInfo.finished)}
</Text>
<PipeSeparator height={7} />
<Calendar height={16} width={16} color={Utils.getRealCSSColor(Color.GREY_500)} />
<Text inline color={Color.GREY_500} font={{ size: 'small' }}>
{timeDistance(executionInfo.finished, Date.now())} ago
</Text>
</Layout.Horizontal>
{executionInfo.started && (
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center' }} className={css.timer}>
<Timer height={16} width={16} color={Utils.getRealCSSColor(Color.GREY_500)} />
<Text inline color={Color.GREY_500} font={{ size: 'small' }}>
{isActive
? timeDistance(executionInfo.started, currentTime, true) // Live update time when status is 'RUNNING'
: timeDistance(executionInfo.started, executionInfo.finished, true)}
</Text>
{executionInfo.finished && (
<>
<PipeSeparator height={7} />
<Calendar height={16} width={16} color={Utils.getRealCSSColor(Color.GREY_500)} />
<Text inline color={Color.GREY_500} font={{ size: 'small' }}>
{timeDistance(executionInfo.finished, currentTime, true)} ago
</Text>
</>
)}
</Layout.Horizontal>
)}
</Container>
)
}

View File

@ -5,6 +5,7 @@ import type { TypesStage } from 'services/code'
import { ExecutionState, ExecutionStatus } from 'components/ExecutionStatus/ExecutionStatus'
import { getStatus } from 'utils/ExecutionUtils'
import { timeDistance } from 'utils/Utils'
import { useLiveTimer } from 'hooks/useLiveTimeHook'
import css from './ExecutionStageList.module.scss'
interface ExecutionStageListProps {
@ -21,6 +22,9 @@ interface ExecutionStageProps {
}
const ExecutionStage: FC<ExecutionStageProps> = ({ stage, isSelected = false, setSelectedStage }) => {
const isActive = stage?.status === ExecutionState.RUNNING
const currentTime = useLiveTimer(isActive)
return (
<Container
className={css.menuItem}
@ -40,7 +44,12 @@ const ExecutionStage: FC<ExecutionStageProps> = ({ stage, isSelected = false, se
{stage.name}
</Text>
<FlexExpander />
<Text style={{ fontSize: '12px' }}>{timeDistance(stage.started, stage.stopped)}</Text>
{stage.started && (stage.stopped || isActive) && (
<Text style={{ fontSize: '12px' }}>
{/* Use live time when running, static time when finished */}
{timeDistance(stage.started, isActive ? currentTime : stage.stopped, true)}
</Text>
)}
</Layout.Horizontal>
</Container>
)

View File

@ -3,13 +3,6 @@
font-weight: 600 !important;
}
.hash {
color: var(--primary-7) !important;
font-family: Roboto Mono !important;
font-size: var(--font-size-small) !important;
font-weight: 500 !important;
}
.pillContainer {
background-color: var(--grey-100) !important;
color: var(--grey-500) !important;

View File

@ -1,6 +1,5 @@
/* eslint-disable */
// This is an auto-generated file
export declare const author: string
export declare const hash: string
export declare const pillContainer: string
export declare const pillText: string

View File

@ -1,11 +1,11 @@
import React from 'react'
import { Avatar, Layout, Text, Utils } from '@harnessio/uicore'
import { Link } from 'react-router-dom'
import { Avatar, Container, Layout, Text, Utils } from '@harnessio/uicore'
import { GitCommit, GitFork, Label } from 'iconoir-react'
import { Color } from '@harnessio/design-system'
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
import { useAppContext } from 'AppContext'
import type { EnumTriggerAction } from 'services/code'
import { CommitActions } from 'components/CommitActions/CommitActions'
import css from './ExecutionText.module.scss'
export enum ExecutionTrigger {
@ -146,17 +146,16 @@ export const ExecutionText: React.FC<ExecutionTextProps> = ({
<Avatar email={authorEmail} name={authorName} size="small" hoverCard={false} />
{componentToRender}
<PipeSeparator height={7} />
<Link
to={routes.toCODECommit({
repoPath: repoPath,
commitRef: commitRef
})}
className={css.hash}
onClick={e => {
e.stopPropagation()
}}>
{commitRef?.slice(0, 6)}
</Link>
<Container onClick={Utils.stopEvent}>
<CommitActions
href={routes.toCODECommit({
repoPath: repoPath,
commitRef: commitRef
})}
sha={commitRef}
enableCopy
/>
</Container>
</Layout.Horizontal>
)
}

View File

@ -94,7 +94,11 @@ export const NewSecretModalButton: React.FC<NewSecretModalButtonProps> = ({
formName="addSecret"
enableReinitialize={true}
validationSchema={yup.object().shape({
name: yup.string().trim().required(),
name: yup
.string()
.trim()
.required()
.matches(/^[a-zA-Z_][a-zA-Z0-9-_.]*$/, getString('validation.nameLogic')),
value: yup.string().trim().required()
})}
validateOnChange
@ -118,7 +122,7 @@ export const NewSecretModalButton: React.FC<NewSecretModalButtonProps> = ({
tooltipProps={{
dataTooltipId: 'secretDescriptionTextField'
}}
inputGroup={{ type: 'password' }}
inputGroup={{ type: 'password', autoComplete: 'new-password' }}
/>
<FormInput.Text
name="description"

View File

@ -3,6 +3,10 @@
width: 100% !important;
border-radius: 4px !important;
border: 1px solid rgba(217, 218, 229, 0.5) !important;
}
.actionsSubContainer {
width: 100% !important;
display: grid !important;
grid-template-columns: repeat(2, 1fr) !important;
gap: 10px !important;

View File

@ -1,3 +1,4 @@
/* eslint-disable */
// This is an auto-generated file
export declare const actionsContainer: string
export declare const actionsSubContainer: string

View File

@ -21,7 +21,7 @@ import { useModalHook } from 'hooks/useModalHook'
import { useStrings } from 'framework/strings'
import type { EnumTriggerAction, OpenapiCreateTriggerRequest, TypesTrigger } from 'services/code'
import { getErrorMessage } from 'utils/Utils'
import { triggerActions } from 'components/PipelineTriggersTab/PipelineTriggersTab'
import { allActions } from 'components/PipelineTriggersTab/PipelineTriggersTab'
import css from './NewTriggerModalButton.module.scss'
export interface TriggerFormData {
@ -92,11 +92,9 @@ export const NewTriggerModalButton: React.FC<NewTriggerModalButtonProps> = ({
validationSchema={yup.object().shape({
name: yup
.string()
.required('name is required')
.matches(
/^[a-zA-Z_][a-zA-Z0-9-_.]*$/,
'name must start with a letter or _ and only contain [a-zA-Z0-9-_.]'
),
.trim()
.required()
.matches(/^[a-zA-Z_][a-zA-Z0-9-_.]*$/, getString('validation.nameLogic')),
actions: yup.array().of(yup.string())
})}
validateOnChange
@ -113,24 +111,29 @@ export const NewTriggerModalButton: React.FC<NewTriggerModalButtonProps> = ({
<Text font={{ variation: FontVariation.FORM_LABEL }} margin={{ bottom: 'xsmall' }}>
{getString('triggers.actions')}
</Text>
<Container className={css.actionsContainer} padding={'large'}>
{triggerActions.map(action => (
<Checkbox
key={action.name}
name="actions"
label={action.name}
value={action.value}
onChange={event => {
if (event.currentTarget.checked) {
formik.setFieldValue('actions', [...formik.values.actions, action.value])
} else {
formik.setFieldValue(
'actions',
formik.values.actions.filter((value: string) => value !== action.value)
)
}
}}
/>
<Container className={css.actionsContainer}>
{allActions.map((actionGroup, index) => (
<Container className={css.actionsSubContainer} padding={'large'} key={index}>
{actionGroup.map(action => (
<Checkbox
key={action.name}
name="actions"
label={action.name}
value={action.value}
checked={formik.values.actions.includes(action.value as EnumTriggerAction)}
onChange={event => {
if (event.currentTarget.checked) {
formik.setFieldValue('actions', [...formik.values.actions, action.value])
} else {
formik.setFieldValue(
'actions',
formik.values.actions.filter((value: string) => value !== action.value)
)
}
}}
/>
))}
</Container>
))}
</Container>
<Layout.Horizontal spacing="small" padding={{ top: 'large' }} style={{ alignItems: 'center' }}>

View File

@ -29,16 +29,24 @@ type TriggerAction = {
value: string
}
export const triggerActions: TriggerAction[] = [
const branchActions: TriggerAction[] = [
{ name: 'Branch Created', value: 'branch_created' },
{ name: 'Branch Updated', value: 'branch_updated' },
{ name: 'Pull Request Branch Updated', value: 'pullreq_branch_updated' },
{ name: 'Branch Updated', value: 'branch_updated' }
]
const pullRequestActions: TriggerAction[] = [
{ name: 'Pull Request Created', value: 'pullreq_created' },
{ name: 'Pull Request Reopened', value: 'pullreq_reopened' },
{ name: 'Pull Request Updated', value: 'pullreq_branch_updated' },
{ name: 'Pull Request Reopened', value: 'pullreq_reopened' }
]
const tagActions: TriggerAction[] = [
{ name: 'Tag Created', value: 'tag_created' },
{ name: 'Tag Updated', value: 'tag_updated' }
]
export const allActions: TriggerAction[][] = [branchActions, pullRequestActions, tagActions]
interface TriggerMenuItemProps {
name: string
lastUpdated: number
@ -177,25 +185,29 @@ const TriggerDetails = ({
/>
</Container>
<div className={css.separator} />
<Container className={css.actionsContainer} padding={'large'}>
{triggerActions.map(action => (
<Checkbox
key={action.name}
name="actions"
label={action.name}
value={action.value}
checked={formik.values.actions.includes(action.value as EnumTriggerAction)}
onChange={event => {
if (event.currentTarget.checked) {
formik.setFieldValue('actions', [...formik.values.actions, action.value])
} else {
formik.setFieldValue(
'actions',
formik.values.actions.filter((value: string) => value !== action.value)
)
}
}}
/>
<Container>
{allActions.map((actionGroup, index) => (
<Container className={css.actionsContainer} padding={'large'} key={index}>
{actionGroup.map(action => (
<Checkbox
key={action.name}
name="actions"
label={action.name}
value={action.value}
checked={formik.values.actions.includes(action.value as EnumTriggerAction)}
onChange={event => {
if (event.currentTarget.checked) {
formik.setFieldValue('actions', [...formik.values.actions, action.value])
} else {
formik.setFieldValue(
'actions',
formik.values.actions.filter((value: string) => value !== action.value)
)
}
}}
/>
))}
</Container>
))}
</Container>
<div className={css.separator} />
@ -203,9 +215,9 @@ const TriggerDetails = ({
spacing="small"
padding={{ top: 'large', left: 'large', right: 'large' }}
style={{ alignItems: 'center' }}>
<Button type="submit" text={getString('edit')} intent={Intent.PRIMARY} disabled={loading} />
<Button type="submit" text={getString('save')} intent={Intent.PRIMARY} disabled={loading} />
<Button
text={getString('triggers.deleteTrigger')}
text={getString('delete')}
intent={Intent.DANGER}
variation={ButtonVariation.SECONDARY}
onClick={() => {
@ -255,46 +267,48 @@ const PipelineTriggersTabs = ({ repoPath, pipeline }: PipelineTriggersTabsProps)
return (
<>
<LoadingSpinner visible={loading} />
{data?.length && (
<Layout.Horizontal padding={'large'}>
<Layout.Vertical padding={'large'}>
<NewTriggerModalButton
modalTitle={getString('triggers.createTrigger')}
text={getString('triggers.newTrigger')}
variation={ButtonVariation.PRIMARY}
icon="plus"
onSuccess={() => refetch()}
repoPath={repoPath}
pipeline={pipeline}
width="150px"
/>
<Layout.Vertical spacing={'large'} className={css.triggerList}>
{data?.map((trigger, index) => (
<TriggerMenuItem
key={trigger.id}
name={trigger.uid as string}
lastUpdated={trigger.updated as number}
setSelectedTrigger={setSelectedTrigger}
index={index}
isSelected={selectedTrigger === index}
/>
))}
<Layout.Horizontal padding={'large'}>
<Layout.Vertical padding={'large'}>
<NewTriggerModalButton
modalTitle={getString('triggers.createTrigger')}
text={getString('triggers.newTrigger')}
variation={ButtonVariation.PRIMARY}
icon="plus"
onSuccess={() => refetch()}
repoPath={repoPath}
pipeline={pipeline}
width="150px"
/>
<Layout.Vertical spacing={'large'} className={css.triggerList}>
{data?.map((trigger, index) => (
<TriggerMenuItem
key={trigger.id}
name={trigger.uid as string}
lastUpdated={trigger.updated as number}
setSelectedTrigger={setSelectedTrigger}
index={index}
isSelected={selectedTrigger === index}
/>
))}
</Layout.Vertical>
</Layout.Vertical>
{data && data?.length > 0 && (
<>
<div className={css.separator} />
<Layout.Vertical padding={'large'}>
<TriggerDetails
name={data?.[selectedTrigger]?.uid as string}
repoPath={repoPath}
pipeline={pipeline}
refetchTriggers={refetch}
setSelectedTrigger={setSelectedTrigger}
initialActions={data?.[selectedTrigger]?.actions as EnumTriggerAction[]}
initialDisabled={data?.[selectedTrigger]?.disabled as boolean}
/>
</Layout.Vertical>
</Layout.Vertical>
<div className={css.separator} />
<Layout.Vertical padding={'large'}>
<TriggerDetails
name={data?.[selectedTrigger]?.uid as string}
repoPath={repoPath}
pipeline={pipeline}
refetchTriggers={refetch}
setSelectedTrigger={setSelectedTrigger}
initialActions={data?.[selectedTrigger]?.actions as EnumTriggerAction[]}
initialDisabled={data?.[selectedTrigger]?.disabled as boolean}
/>
</Layout.Vertical>
</Layout.Horizontal>
)}
</>
)}
</Layout.Horizontal>
</>
)
}

View File

@ -82,7 +82,11 @@ const useUpdateSecretModal = () => {
formName="addSecret"
enableReinitialize={true}
validationSchema={yup.object().shape({
name: yup.string().trim().required(),
name: yup
.string()
.trim()
.required()
.matches(/^[a-zA-Z_][a-zA-Z0-9-_.]*$/, getString('validation.nameLogic')),
value: yup.string().trim().required()
})}
validateOnChange

View File

@ -644,6 +644,7 @@ export interface StringsMap {
'validation.gitTagNameInvalid': string
'validation.nameInvalid': string
'validation.nameIsRequired': string
'validation.nameLogic': string
'validation.newPasswordRequired': string
'validation.repoNamePatternIsNotValid': string
'validation.spaceNamePatternIsNotValid': string

View File

@ -0,0 +1,28 @@
import { useState, useEffect } from 'react'
/**
* useLiveTimer returns the current time, updated every second.
*
* @param isActive - If true, the timer is active and updates every second.
*/
export function useLiveTimer(isActive: boolean): number {
const [currentTime, setCurrentTime] = useState(Date.now())
useEffect(() => {
let intervalId: NodeJS.Timeout
if (isActive) {
intervalId = setInterval(() => {
setCurrentTime(Date.now())
}, 1000)
}
return () => {
if (intervalId) {
clearInterval(intervalId)
}
}
}, [isActive])
return currentTime
}

View File

@ -125,6 +125,7 @@ validation:
newPasswordRequired: New password is a required field
confirmPasswordRequired: Confirm Password is a required field
spaceNamePatternIsNotValid: "Name can only contain alphanumerics, '-', '_', '.', and '$'"
nameLogic: name must start with a letter or _ and only contain [a-zA-Z0-9-_.]
commitMessage: Commit message
optionalExtendedDescription: Optional extended description
optional: Optional

View File

@ -1,7 +1,9 @@
import React, { useMemo, useState } from 'react'
import { Container, Layout } from '@harnessio/uicore'
import { Container, Layout, Utils } from '@harnessio/uicore'
import { Render } from 'react-jsx-match'
import { useHistory, useRouteMatch } from 'react-router-dom'
import { Lock } from 'iconoir-react'
import { Color } from '@harnessio/design-system'
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
import { useStrings } from 'framework/strings'
import type { TypesSpace } from 'services/code'
@ -154,9 +156,9 @@ export const DefaultMenu: React.FC = () => {
<Render when={selectedSpace}>
{/* icon is placeholder */}
<NavMenuItem
icon="lock"
label={getString('pageTitle.secrets')}
to={routes.toCODESecrets({ space: selectedSpace?.path as string })}
customIcon={<Lock color={Utils.getRealCSSColor(Color.GREY_700)} />}
/>
</Render>
)}

View File

@ -81,3 +81,8 @@
}
}
}
.customIcon {
height: 16px;
padding-right: var(--spacing-small) !important;
}

View File

@ -1,5 +1,6 @@
/* eslint-disable */
// This is an auto-generated file
export declare const customIcon: string
export declare const highlighted: string
export declare const link: string
export declare const selected: string

View File

@ -15,6 +15,7 @@ interface NavMenuItemProps extends NavLinkProps {
isSelected?: boolean
isDeselected?: boolean
isHighlighted?: boolean
customIcon?: React.ReactNode
}
export const NavMenuItem: React.FC<NavMenuItemProps> = ({
@ -28,6 +29,7 @@ export const NavMenuItem: React.FC<NavMenuItemProps> = ({
isDeselected,
isHighlighted,
children,
customIcon,
...others
}) => (
<Link
@ -39,7 +41,8 @@ export const NavMenuItem: React.FC<NavMenuItemProps> = ({
activeClassName={isDeselected ? '' : css.selected}
{...others}>
{children}
<Text icon={icon} rightIcon={rightIcon} className={css.text} {...textProps}>
{customIcon && <span className={css.customIcon}>{customIcon}</span>}
<Text icon={customIcon ? undefined : icon} rightIcon={rightIcon} className={css.text} {...textProps}>
{label}
</Text>
</Link>

View File

@ -1,10 +1,10 @@
.main {
min-height: var(--page-height);
height: calc(100vh - 112px) !important;
background-color: var(--primary-bg) !important;
}
.container {
height: calc(100vh - var(--page-header-height));
.pageBody {
min-height: 100% !important;
}
.withError {

View File

@ -1,5 +1,5 @@
/* eslint-disable */
// This is an auto-generated file
export declare const container: string
export declare const main: string
export declare const pageBody: string
export declare const withError: string

View File

@ -80,7 +80,7 @@ const Execution = () => {
]
}
executionInfo={{
message: execution?.message as string,
message: (execution?.message || execution?.title) as string,
authorName: execution?.author_name as string,
authorEmail: execution?.author_email as string,
source: execution?.source as string,
@ -91,7 +91,7 @@ const Execution = () => {
}}
/>
<PageBody
className={cx({ [css.withError]: !!error })}
className={cx(css.pageBody, { [css.withError]: !!error })}
error={error ? getErrorMessage(error || executionError) : null}
retryOnError={voidFn(refetch)}
noData={{

View File

@ -28,11 +28,12 @@ import { usePageIndex } from 'hooks/usePageIndex'
import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
import { ExecutionStatus } from 'components/ExecutionStatus/ExecutionStatus'
import { ExecutionState, ExecutionStatus } from 'components/ExecutionStatus/ExecutionStatus'
import { getStatus } from 'utils/ExecutionUtils'
import useSpaceSSE from 'hooks/useSpaceSSE'
import { ExecutionText, ExecutionTrigger } from 'components/ExecutionText/ExecutionText'
import useRunPipelineModal from 'components/RunPipelineModal/RunPipelineModal'
import { useLiveTimer } from 'hooks/useLiveTimeHook'
import noExecutionImage from '../RepositoriesListing/no-repo.svg'
import css from './ExecutionList.module.scss'
@ -44,6 +45,7 @@ const ExecutionList = () => {
const pageBrowser = useQueryParams<PageBrowserProps>()
const pageInit = pageBrowser.page ? parseInt(pageBrowser.page) : 1
const [page, setPage] = usePageIndex(pageInit)
const currentTime = useLiveTimer(true)
const { repoMetadata, error, loading, refetch, space } = useGetRepositoryMetadata()
@ -111,7 +113,7 @@ const ExecutionList = () => {
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center' }}>
<ExecutionStatus status={getStatus(record.status)} iconOnly noBackground iconSize={20} isCi />
<Text className={css.number}>{`#${record.number}.`}</Text>
<Text className={css.desc}>{record.message}</Text>
<Text className={css.desc}>{record.message || record.title}</Text>
</Layout.Horizontal>
<ExecutionText
authorEmail={record.author_email as string}
@ -133,29 +135,36 @@ const ExecutionList = () => {
width: '180px',
Cell: ({ row }: CellProps<TypesExecution>) => {
const record = row.original
// Determine if the execution is active.
const isActive = record.status === ExecutionState.RUNNING
return (
<Layout.Vertical spacing={'small'}>
{record?.started && record?.finished && (
{record?.started && (isActive || record?.finished) && (
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center' }}>
<Timer color={Utils.getRealCSSColor(Color.GREY_500)} />
<Text inline color={Color.GREY_500} lineClamp={1} width={180} font={{ size: 'small' }}>
{timeDistance(record.started, record.finished)}
{/* Use live time when running, static time when finished */}
{timeDistance(record.started, isActive ? currentTime : record.finished, true)}
</Text>
</Layout.Horizontal>
)}
{record?.finished && (
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center' }}>
<Calendar color={Utils.getRealCSSColor(Color.GREY_500)} />
<Text inline color={Color.GREY_500} lineClamp={1} width={180} font={{ size: 'small' }}>
{timeDistance(record.finished, currentTime, true)} ago
</Text>
</Layout.Horizontal>
)}
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center' }}>
<Calendar color={Utils.getRealCSSColor(Color.GREY_500)} />
<Text inline color={Color.GREY_500} lineClamp={1} width={180} font={{ size: 'small' }}>
{timeDistance(record.started, Date.now())} ago
</Text>
</Layout.Horizontal>
</Layout.Vertical>
)
},
disableSortBy: true
}
],
[getString, repoMetadata?.path, routes]
[currentTime, getString, repoMetadata?.path]
)
return (

View File

@ -39,13 +39,6 @@
font-weight: 500 !important;
}
.hash {
color: var(--primary-7) !important;
font-family: Roboto Mono !important;
font-size: var(--font-size-small) !important;
font-weight: 500 !important;
}
.triggerLayout {
align-items: center !important;
}

View File

@ -3,7 +3,6 @@
export declare const author: string
export declare const avatar: string
export declare const desc: string
export declare const hash: string
export declare const layout: string
export declare const main: string
export declare const nameContainer: string

View File

@ -18,7 +18,7 @@ import { Color } from '@harnessio/design-system'
import cx from 'classnames'
import type { CellProps, Column } from 'react-table'
import Keywords from 'react-keywords'
import { Link, useHistory } from 'react-router-dom'
import { useHistory } from 'react-router-dom'
import { useGet, useMutate } from 'restful-react'
import { Calendar, Timer, GitFork } from 'iconoir-react'
import { String, useStrings } from 'framework/strings'
@ -40,6 +40,8 @@ import useNewPipelineModal from 'components/NewPipelineModal/NewPipelineModal'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import useSpaceSSE from 'hooks/useSpaceSSE'
import { useConfirmAct } from 'hooks/useConfirmAction'
import { useLiveTimer } from 'hooks/useLiveTimeHook'
import { CommitActions } from 'components/CommitActions/CommitActions'
import noPipelineImage from '../RepositoriesListing/no-repo.svg'
import css from './PipelineList.module.scss'
@ -52,6 +54,7 @@ const PipelineList = () => {
const pageInit = pageBrowser.page ? parseInt(pageBrowser.page) : 1
const [page, setPage] = usePageIndex(pageInit)
const space = useGetSpaceParam()
const currentTime = useLiveTimer(true)
const { repoMetadata, error, loading, refetch } = useGetRepositoryMetadata()
@ -156,17 +159,16 @@ const PipelineList = () => {
</>
)}
<PipeSeparator height={7} />
<Link
to={routes.toCODECommit({
repoPath: repoMetadata?.path as string,
commitRef: record.after as string
})}
className={css.hash}
onClick={e => {
e.stopPropagation()
}}>
{record.after?.slice(0, 6)}
</Link>
<Container onClick={Utils.stopEvent}>
<CommitActions
href={routes.toCODECommit({
repoPath: repoMetadata?.path as string,
commitRef: record?.after as string
})}
sha={record?.after as string}
enableCopy
/>
</Container>
</Layout.Horizontal>
</Layout.Vertical>
) : (
@ -180,20 +182,27 @@ const PipelineList = () => {
Cell: ({ row }: CellProps<TypesPipeline>) => {
const record = row.original.execution
const isActive = record?.status === ExecutionState.RUNNING
return record ? (
<Layout.Vertical spacing={'small'}>
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center' }}>
<Timer color={Utils.getRealCSSColor(Color.GREY_500)} />
<Text inline color={Color.GREY_500} lineClamp={1} width={180} font={{ size: 'small' }}>
{timeDistance(record.started, record.finished)}
</Text>
</Layout.Horizontal>
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center' }}>
<Calendar color={Utils.getRealCSSColor(Color.GREY_500)} />
<Text inline color={Color.GREY_500} lineClamp={1} width={180} font={{ size: 'small' }}>
{timeDistance(record.started, Date.now())} ago
</Text>
</Layout.Horizontal>
{record?.started && (isActive || record?.finished) && (
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center' }}>
<Timer color={Utils.getRealCSSColor(Color.GREY_500)} />
<Text inline color={Color.GREY_500} lineClamp={1} width={180} font={{ size: 'small' }}>
{/* Use live time when running, static time when finished */}
{timeDistance(record.started, isActive ? currentTime : record.finished, true)}
</Text>
</Layout.Horizontal>
)}
{record?.finished && (
<Layout.Horizontal spacing={'small'} style={{ alignItems: 'center' }}>
<Calendar color={Utils.getRealCSSColor(Color.GREY_500)} />
<Text inline color={Color.GREY_500} lineClamp={1} width={180} font={{ size: 'small' }}>
{timeDistance(record.finished, currentTime, true)} ago
</Text>
</Layout.Horizontal>
)}
</Layout.Vertical>
) : (
<div className={css.spacer} />

View File

@ -105,20 +105,35 @@ export const displayDateTime = (value: number): string | null => {
return value ? moment.unix(value / 1000).format(DEFAULT_DATE_FORMAT) : null
}
export const timeDistance = (date1 = 0, date2 = 0) => {
export const timeDistance = (date1 = 0, date2 = 0, onlyHighestDenomination = false) => {
let distance = Math.abs(date1 - date2)
if (!distance) {
return ''
}
const days = Math.floor(distance / (24 * 3600000)) // 24 hours * 60 minutes * 60 seconds * 1000 milliseconds
const days = Math.floor(distance / (24 * 3600000))
if (onlyHighestDenomination && days) {
return days + 'd'
}
distance -= days * 24 * 3600000
const hours = Math.floor(distance / 3600000)
if (onlyHighestDenomination && hours) {
return hours + 'h'
}
distance -= hours * 3600000
const minutes = Math.floor(distance / 60000)
if (onlyHighestDenomination && minutes) {
return minutes + 'm'
}
distance -= minutes * 60000
const seconds = Math.floor(distance / 1000)
if (onlyHighestDenomination) {
return seconds + 's'
}
return `${days ? days + 'd ' : ''}${hours ? hours + 'h ' : ''}${
minutes ? minutes + 'm' : hours || days ? '0m' : ''