Skip to content

Commit

Permalink
feat(ui): Display pretty cron schedule (#5088)
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Collins <alex_collins@intuit.com>
  • Loading branch information
alexec authored Feb 11, 2021
1 parent 1a0889c commit 3b7e373
Show file tree
Hide file tree
Showing 15 changed files with 102 additions and 42 deletions.
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"chartjs-plugin-annotation": "^0.5.7",
"classnames": "^2.2.5",
"cron-parser": "^2.16.3",
"cronstrue": "^1.109.0",
"dagre": "^0.8.5",
"formik": "^2.1.2",
"history": "^4.7.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import {Page, SlidingPanel} from 'argo-ui';
import {Page, SlidingPanel, Ticker} from 'argo-ui';
import * as React from 'react';
import {Link, RouteComponentProps} from 'react-router-dom';
import * as models from '../../../../models';
import {uiUrl} from '../../../shared/base';
import {BasePage} from '../../../shared/components/base-page';
import {DurationFromNow} from '../../../shared/components/duration-panel';
import {ErrorNotice} from '../../../shared/components/error-notice';
import {ExampleManifests} from '../../../shared/components/example-manifests';
import {InfoIcon} from '../../../shared/components/fa-icons';
Expand All @@ -18,6 +17,7 @@ import {Footnote} from '../../../shared/footnote';
import {services} from '../../../shared/services';
import {Utils} from '../../../shared/utils';
import {CronWorkflowCreator} from '../cron-workflow-creator';
import {PrettySchedule} from '../pretty-schedule';

require('./cron-workflow-list.scss');

Expand Down Expand Up @@ -125,9 +125,10 @@ export class CronWorkflowList extends BasePage<RouteComponentProps<any>, State>
<div className='columns small-1' />
<div className='columns small-3'>NAME</div>
<div className='columns small-2'>NAMESPACE</div>
<div className='columns small-2'>SCHEDULE</div>
<div className='columns small-2'>CREATED</div>
<div className='columns small-2'>NEXT RUN</div>
<div className='columns small-1'>SCHEDULE</div>
<div className='columns small-3' />
<div className='columns small-1'>CREATED</div>
<div className='columns small-1'>NEXT RUN</div>
</div>
{this.state.cronWorkflows.map(w => (
<Link
Expand All @@ -137,12 +138,15 @@ export class CronWorkflowList extends BasePage<RouteComponentProps<any>, State>
<div className='columns small-1'>{w.spec.suspend ? <i className='fa fa-pause' /> : <i className='fa fa-clock' />}</div>
<div className='columns small-3'>{w.metadata.name}</div>
<div className='columns small-2'>{w.metadata.namespace}</div>
<div className='columns small-2'>{w.spec.schedule}</div>
<div className='columns small-2'>
<div className='columns small-1'>{w.spec.schedule}</div>
<div className='columns small-3'>
<PrettySchedule schedule={w.spec.schedule} />
</div>
<div className='columns small-1'>
<Timestamp date={w.metadata.creationTimestamp} />
</div>
<div className='columns small-2'>
{w.spec.suspend ? '' : <DurationFromNow getDate={() => getNextScheduledTime(w.spec.schedule, w.spec.timezone)} />}
<div className='columns small-1'>
{w.spec.suspend ? '' : <Ticker intervalMs={1000}>{() => <Timestamp date={getNextScheduledTime(w.spec.schedule, w.spec.timezone)} />}</Ticker>}
</div>
</Link>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as React from 'react';
import {ConcurrencyPolicy, CronWorkflowSpec} from '../../../models';
import {NumberInput} from '../../shared/components/number-input';
import {TextInput} from '../../shared/components/text-input';
import {ScheduleValidator} from './schedule-validator';

export const CronWorkflowSpecEditor = ({onChange, spec}: {spec: CronWorkflowSpec; onChange: (spec: CronWorkflowSpec) => void}) => {
return (
Expand All @@ -12,6 +13,7 @@ export const CronWorkflowSpecEditor = ({onChange, spec}: {spec: CronWorkflowSpec
<div className='columns small-3'>Schedule</div>
<div className='columns small-9'>
<TextInput value={spec.schedule} onChange={schedule => onChange({...spec, schedule})} />
<ScheduleValidator schedule={spec.schedule} />
</div>
</div>
<div className='row white-box__details-row'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {CronWorkflowSpec, CronWorkflowStatus} from '../../../models';
import {Timestamp} from '../../shared/components/timestamp';
import {ConditionsPanel} from '../../shared/conditions-panel';
import {WorkflowLink} from '../../workflows/components/workflow-link';
import {PrettySchedule} from './pretty-schedule';

const parser = require('cron-parser');
export const CronWorkflowStatusViewer = ({spec, status}: {spec: CronWorkflowSpec; status: CronWorkflowStatus}) => {
Expand All @@ -15,6 +16,14 @@ export const CronWorkflowStatusViewer = ({spec, status}: {spec: CronWorkflowSpec
<div className='white-box__details'>
{[
{title: 'Active', value: status.active ? getCronWorkflowActiveWorkflowList(status.active) : <i>No Workflows Active</i>},
{
title: 'Schedule',
value: (
<>
<code>{spec.schedule}</code> <PrettySchedule schedule={spec.schedule} />
</>
)
},
{title: 'Last Scheduled Time', value: <Timestamp date={status.lastScheduledTime} />},
{
title: 'Next Scheduled Time',
Expand Down
21 changes: 21 additions & 0 deletions ui/src/app/cron-workflows/components/pretty-schedule.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React = require('react');

const x = require('cronstrue');

/*
https://github.com/bradymholt/cRonstrue
vs
https://github.com/robfig/cron
I think we must assume that these libraries (or any two libraries) will never be exactly the same and accept that
sometime it'll not work as expected. Therefore, we must let the user know about this.
*/

export const PrettySchedule = ({schedule}: {schedule: string}) => {
try {
const pretty = x.toString(schedule);
return <span title={pretty}>{pretty}</span>;
} catch (e) {
return <>{e.toString()}</>;
}
};
20 changes: 20 additions & 0 deletions ui/src/app/cron-workflows/components/schedule-validator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React = require('react');
import {SuccessIcon, WarningIcon} from '../../shared/components/fa-icons';

const x = require('cronstrue');

export const ScheduleValidator = ({schedule}: {schedule: string}) => {
try {
return (
<span>
<SuccessIcon /> {x.toString(schedule)}
</span>
);
} catch (e) {
return (
<span>
<WarningIcon /> Schedule maybe invalid: {e.toString()}
</span>
);
}
};
18 changes: 0 additions & 18 deletions ui/src/app/shared/components/duration-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import * as moment from 'moment';
import * as React from 'react';
import Moment from 'react-moment';
import {NODE_PHASE, NodePhase} from '../../../models';
import {formatDuration} from '../duration';
import {ProgressLine} from './progress-line';
Expand All @@ -19,19 +17,3 @@ export const DurationPanel = (props: {phase: NodePhase; duration: number; estima
}
return <>{formatDuration(props.duration)}</>;
};

export const DurationFromNow = ({getDate, frequency = 1000}: {getDate: () => string; frequency?: number}) => {
const [now, setNow] = React.useState(moment());
const [date, setDate] = React.useState(getDate);
React.useEffect(() => {
const interval = setInterval(() => {
setNow(moment());
setDate(getDate);
}, frequency);
return () => {
clearInterval(interval);
};
}, []);

return <Moment duration={now} date={date} format='dd:hh:mm:ss' />;
};
1 change: 1 addition & 0 deletions ui/src/app/shared/components/fa-icons.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';

export const InfoIcon = () => <i className='fa fa-info-circle' />;
export const SuccessIcon = () => <i className='fa fa-check-circle status-icon--success' />;
export const WarningIcon = () => <i className='fa fa-exclamation-triangle status-icon--pending' />;
export const ErrorIcon = () => <i className='fa fa-exclamation-circle status-icon--error' />;
11 changes: 6 additions & 5 deletions ui/src/app/shared/components/timestamp.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import {Ticker} from 'argo-ui';
import * as React from 'react';
import Moment from 'react-moment';
import {ago} from '../duration';

export const Timestamp = ({date}: {date: string | number}) => {
export const Timestamp = ({date}: {date: Date | string | number}) => {
return (
<span>
{date === null ? (
'-'
) : (
<Moment fromNow={true} withTitle={true}>
{date}
</Moment>
<span title={date.toString()}>
<Ticker intervalMs={1000}>{() => ago(new Date(date))}</Ticker>
</span>
)}
</span>
);
Expand Down
6 changes: 3 additions & 3 deletions ui/src/app/shared/cron.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import parser = require('cron-parser');

export function getNextScheduledTime(schedule: string, tz: string): string {
let out = '';
export function getNextScheduledTime(schedule: string, tz: string): Date {
let out: Date;
try {
out = parser
.parseExpression(schedule, {utc: !tz, tz})
.next()
.toISOString();
.toDate();
} catch (e) {
// Do nothing
}
Expand Down
16 changes: 13 additions & 3 deletions ui/src/app/shared/duration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import * as models from '../../models';
* @param sigfigs Level of significant figures to show
*/

export function formatDuration(seconds: number, sigfigs?: number) {
let remainingSeconds = Math.round(seconds);
export function formatDuration(seconds: number, sigfigs = 1) {
let remainingSeconds = Math.abs(Math.round(seconds));
let formattedDuration = '';
const figs = [];

Expand Down Expand Up @@ -37,7 +37,7 @@ export function formatDuration(seconds: number, sigfigs?: number) {
formattedDuration += remainingSeconds + 's';
}

if (sigfigs && sigfigs <= figs.length) {
if (sigfigs <= figs.length) {
formattedDuration = '';
for (let i = 0; i < sigfigs; i++) {
formattedDuration += figs[i];
Expand Down Expand Up @@ -67,3 +67,13 @@ export function wfDuration(status: models.WorkflowStatus) {
}
return ((status.finishedAt ? new Date(status.finishedAt) : new Date()).getTime() - new Date(status.startedAt).getTime()) / 1000;
}

export const ago = (date: Date) => {
const secondsAgo = (new Date().getTime() - date.getTime()) / 1000;
const duration = formatDuration(secondsAgo);
if (secondsAgo < 0) {
return 'in ' + duration;
} else {
return duration + ' ago';
}
};
2 changes: 2 additions & 0 deletions ui/src/app/shared/services/workflows-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class WorkflowsService {
'items.metadata.creationTimestamp',
'items.metadata.labels',
'items.status.phase',
'items.status.message',
'items.status.finishedAt',
'items.status.startedAt',
'items.status.estimatedDuration',
Expand Down Expand Up @@ -86,6 +87,7 @@ export class WorkflowsService {
'result.object.metadata.uid',
'result.object.status.finishedAt',
'result.object.status.phase',
'result.object.status.message',
'result.object.status.startedAt',
'result.object.status.estimatedDuration',
'result.object.status.progress',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,11 @@ export class WorkflowsList extends BasePage<RouteComponentProps<any>, State> {
<div className='row small-11'>
<div className='columns small-3'>NAME</div>
<div className='columns small-2'>NAMESPACE</div>
<div className='columns small-2'>STARTED</div>
<div className='columns small-2'>FINISHED</div>
<div className='columns small-1'>STARTED</div>
<div className='columns small-1'>FINISHED</div>
<div className='columns small-1'>DURATION</div>
<div className='columns small-1'>PROGRESS</div>
<div className='columns small-2'>MESSAGE</div>
<div className='columns small-1'>DETAILS</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,17 @@ export class WorkflowsRow extends React.Component<WorkflowsRowProps, WorkflowRow
<Link to={uiUrl(`workflows/${wf.metadata.namespace}/${wf.metadata.name}`)} className='row small-11'>
<div className='columns small-3'>{wf.metadata.name}</div>
<div className='columns small-2'>{wf.metadata.namespace}</div>
<div className='columns small-2'>
<div className='columns small-1'>
<Timestamp date={wf.status.startedAt} />
</div>
<div className='columns small-2'>
<div className='columns small-1'>
<Timestamp date={wf.status.finishedAt} />
</div>
<div className='columns small-1'>
<Ticker>{() => <DurationPanel phase={wf.status.phase} duration={wfDuration(wf.status)} estimatedDuration={wf.status.estimatedDuration} />}</Ticker>
</div>
<div className='columns small-1'>{wf.status.progress || '-'}</div>
<div className='columns small-2'>{wf.status.message || '-'}</div>
<div className='columns small-1'>
<div className='workflows-list__labels-container'>
<div
Expand Down
5 changes: 5 additions & 0 deletions ui/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2970,6 +2970,11 @@ cron-parser@^2.16.3:
is-nan "^1.3.0"
moment-timezone "^0.5.31"

cronstrue@^1.109.0:
version "1.109.0"
resolved "https://registry.yarnpkg.com/cronstrue/-/cronstrue-1.109.0.tgz#9c17e0c392eb32ae6678b5f02d65042cfcea554a"
integrity sha512-l4ShtlLtQmg5Nc7kDyD0VekVHPw91sLVn8I57TFssnDmIA9G8BObNrkDLMS34+k7N7WgjQE9hCQfv7Zfv+jUHg==

cross-fetch@^3.0.6:
version "3.0.6"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.0.6.tgz#3a4040bc8941e653e0e9cf17f29ebcd177d3365c"
Expand Down

0 comments on commit 3b7e373

Please sign in to comment.