mirror of
https://github.com/harness/drone.git
synced 2025-05-30 19:23:07 +00:00
small ui fixes in pipelines (#501)
This commit is contained in:
parent
4ff032c968
commit
a6dcc0a6ef
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -3,3 +3,4 @@
|
||||
export declare const lineNumber: string
|
||||
export declare const log: string
|
||||
export declare const logLayout: string
|
||||
export declare const time: string
|
||||
|
@ -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>
|
||||
)
|
||||
})}
|
||||
|
@ -33,3 +33,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
user-select: none !important;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
// This is an auto-generated file
|
||||
export declare const actionsContainer: string
|
||||
export declare const actionsSubContainer: string
|
||||
|
@ -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' }}>
|
||||
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
28
web/src/hooks/useLiveTimeHook.tsx
Normal file
28
web/src/hooks/useLiveTimeHook.tsx
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -81,3 +81,8 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.customIcon {
|
||||
height: 16px;
|
||||
padding-right: var(--spacing-small) !important;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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={{
|
||||
|
@ -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 (
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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} />
|
||||
|
@ -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' : ''
|
||||
|
Loading…
x
Reference in New Issue
Block a user