diff --git a/ui/src/app/shared/components/input-filter.tsx b/ui/src/app/shared/components/input-filter.tsx index b723915f2133..e870a7f97efb 100644 --- a/ui/src/app/shared/components/input-filter.tsx +++ b/ui/src/app/shared/components/input-filter.tsx @@ -1,5 +1,5 @@ import {Autocomplete} from 'argo-ui'; -import * as React from 'react'; +import React, {useState} from 'react'; interface InputProps { value: string; @@ -8,68 +8,60 @@ interface InputProps { onChange: (input: string) => void; } -interface InputState { - value: string; - localCache: string[]; - error?: Error; -} +export function InputFilter(props: InputProps) { + const [value, setValue] = useState(props.value); + const [localCache, setLocalCache] = useState((localStorage.getItem(props.name + '_inputs') || '').split(',').filter(item => item !== '')); -export class InputFilter extends React.Component { - constructor(props: Readonly) { - super(props); - this.state = { - value: props.value, - localCache: (localStorage.getItem(this.props.name + '_inputs') || '').split(',').filter(value => value !== '') - }; + function setValueAndCache(newValue: string) { + setLocalCache(currentCache => { + const updatedCache = [...currentCache]; + if (!updatedCache.includes(newValue)) { + updatedCache.unshift(newValue); + } + while (updatedCache.length > 5) { + updatedCache.pop(); + } + localStorage.setItem(props.name + '_inputs', updatedCache.join(',')); + return updatedCache; + }); + setValue(newValue); } - public render() { + function renderInput(inputProps: React.HTMLProps) { return ( - <> - this.setState({value})} - onSelect={value => { - this.setState({value}); - this.props.onChange(value); - }} - renderInput={inputProps => ( - { - if (event.keyCode === 13) { - this.setValueAndCache(event.currentTarget.value); - this.props.onChange(this.state.value); - } - }} - className='argo-field' - placeholder={this.props.placeholder} - /> - )} - /> - { - this.setState({value: ''}); - this.props.onChange(''); - }}> - - - + { + if (event.keyCode === 13) { + setValueAndCache(event.currentTarget.value); + props.onChange(value); + } + }} + className='argo-field' + placeholder={props.placeholder} + /> ); } - private setValueAndCache(value: string) { - this.setState(state => { - const localCache = state.localCache; - if (!state.localCache.includes(value)) { - localCache.unshift(value); - } - while (localCache.length > 5) { - localCache.pop(); - } - localStorage.setItem(this.props.name + '_inputs', localCache.join(',')); - return {value, localCache}; - }); - } + return ( + <> + setValue(newValue)} + onSelect={newValue => { + setValue(newValue); + props.onChange(newValue); + }} + renderInput={renderInput} + /> + { + setValue(''); + props.onChange(''); + }}> + + + + ); } diff --git a/ui/src/app/workflows/components/workflow-timeline/workflow-timeline.tsx b/ui/src/app/workflows/components/workflow-timeline/workflow-timeline.tsx index 744ea202554c..7edbf9cf50cf 100644 --- a/ui/src/app/workflows/components/workflow-timeline/workflow-timeline.tsx +++ b/ui/src/app/workflows/components/workflow-timeline/workflow-timeline.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import moment from 'moment'; -import * as React from 'react'; +import React, {useEffect, useRef, useState} from 'react'; import {fromEvent, interval, Subscription} from 'rxjs'; import * as models from '../../../../models'; @@ -11,6 +11,7 @@ require('./workflow-timeline.scss'); const ROUND_START_DIFF_MS = 1000; const NODE_NAME_WIDTH = 250; const MIN_WIDTH = 800; +const COMPLETED_PHASES = [models.NODE_PHASE.ERROR, models.NODE_PHASE.SUCCEEDED, models.NODE_PHASE.SKIPPED, models.NODE_PHASE.OMITTED, models.NODE_PHASE.FAILED]; interface WorkflowTimelineProps { workflow: models.Workflow; @@ -18,128 +19,114 @@ interface WorkflowTimelineProps { nodeClicked?: (node: models.NodeStatus) => any; } -interface WorkflowTimelineState { - parentWidth: number; - now: moment.Moment; -} - -export class WorkflowTimeline extends React.Component { - private container: HTMLElement; - private resizeSubscription: Subscription; - private refreshSubscription: Subscription; +export function WorkflowTimeline(props: WorkflowTimelineProps) { + const [parentWidth, setParentWidth] = useState(0); + const [now, setNow] = useState(moment()); - constructor(props: WorkflowTimelineProps) { - super(props); - this.state = {parentWidth: 0, now: moment()}; - this.ensureRunningWorkflowRefreshing(props.workflow); - } + const containerRef = useRef(null); + const resizeSubscription = useRef(null); + const refreshSubscription = useRef(null); - public componentDidMount() { - this.resizeSubscription = fromEvent(window, 'resize').subscribe(() => this.updateWidth()); - this.updateWidth(); - } + useEffect(() => { + resizeSubscription.current = fromEvent(window, 'resize').subscribe(updateWidth); + updateWidth(); - public componentWillReceiveProps(nextProps: WorkflowTimelineProps) { - this.ensureRunningWorkflowRefreshing(nextProps.workflow); - } + return () => { + resizeSubscription.current?.unsubscribe(); + refreshSubscription.current?.unsubscribe(); + }; + }, []); - public componentWillUnmount() { - if (this.resizeSubscription) { - this.resizeSubscription.unsubscribe(); - this.resizeSubscription = null; + useEffect(() => { + const isCompleted = props.workflow?.status && COMPLETED_PHASES.includes(props.workflow.status.phase); + if (!refreshSubscription.current && !isCompleted) { + refreshSubscription.current = interval(1000).subscribe(() => { + setNow(moment()); + }); + } else if (refreshSubscription.current && isCompleted) { + refreshSubscription.current.unsubscribe(); + refreshSubscription.current = null; } - if (this.refreshSubscription) { - this.refreshSubscription.unsubscribe(); - this.refreshSubscription = null; + }, [props.workflow]); + + function updateWidth() { + if (containerRef.current) { + setParentWidth((containerRef.current.offsetParent || window.document.body).clientWidth - NODE_NAME_WIDTH); } } - public render() { - if (!this.props.workflow.status.nodes) { - return

No nodes

; - } - const nodes = Object.keys(this.props.workflow.status.nodes) - .map(id => { - const node = this.props.workflow.status.nodes[id]; - node.finishedAt = node.finishedAt || this.state.now.format(); - node.startedAt = node.startedAt || this.state.now.format(); - return node; - }) - .filter(node => node.startedAt && node.type === 'Pod') - .sort((first, second) => { - const diff = moment(first.startedAt).diff(second.startedAt); - // If node started almost at the same time then sort by end time - if (diff <= 2) { - return moment(first.finishedAt).diff(second.finishedAt); - } - return diff; - }); - if (nodes.length === 0) { - return
; - } - const timelineStart = moment(nodes[0].startedAt).valueOf(); - const timelineEnd = nodes.map(node => moment(node.finishedAt).valueOf()).reduce((first, second) => Math.max(first, second), moment(timelineStart).valueOf()); - - const timeToLeft = (time: number) => ((time - timelineStart) / (timelineEnd - timelineStart)) * Math.max(this.state.parentWidth, MIN_WIDTH) + NODE_NAME_WIDTH; - const groups = nodes.map(node => ({ - startedAt: moment(node.startedAt).valueOf(), - finishedAt: moment(node.finishedAt).valueOf(), - nodes: [ - Object.assign({}, node, { - left: timeToLeft(moment(node.startedAt).valueOf()), - width: timeToLeft(moment(node.finishedAt).valueOf()) - timeToLeft(moment(node.startedAt).valueOf()) - }) - ] - })); - for (let i = groups.length - 1; i >= 1; i--) { - const cur = groups[i]; - const next = groups[i - 1]; - if (moment(cur.startedAt).diff(next.finishedAt, 'milliseconds') < 0 && moment(next.startedAt).diff(cur.startedAt, 'milliseconds') < ROUND_START_DIFF_MS) { - next.nodes = next.nodes.concat(cur.nodes); - next.finishedAt = nodes.map(node => moment(node.finishedAt).valueOf()).reduce((first, second) => Math.max(first, second), next.finishedAt.valueOf()); - groups.splice(i, 1); + if (!props.workflow.status.nodes) { + return

No nodes

; + } + + const nodes = Object.keys(props.workflow.status.nodes) + .map(id => { + const node = props.workflow.status.nodes[id]; + node.finishedAt = node.finishedAt || now.format(); + node.startedAt = node.startedAt || now.format(); + return node; + }) + .filter(node => node.startedAt && node.type === 'Pod') + .sort((first, second) => { + const diff = moment(first.startedAt).diff(second.startedAt); + if (diff <= 2) { + return moment(first.finishedAt).diff(second.finishedAt); } - } - return ( -
(this.container = container)} style={{width: Math.max(this.state.parentWidth, MIN_WIDTH) + NODE_NAME_WIDTH}}> -
-
- {groups.map(group => [ -
- {moment(group.startedAt).format('hh:mm')} -
, - ...group.nodes.map(node => ( -
this.props.nodeClicked && this.props.nodeClicked(node)}> -
- {Utils.shortNodeName(node)} -
-
-
- )) - ])} -
- ); + return diff; + }); + + if (nodes.length === 0) { + return
; } - public updateWidth() { - if (this.container) { - this.setState({parentWidth: (this.container.offsetParent || window.document.body).clientWidth - NODE_NAME_WIDTH}); - } + const timelineStart = moment(nodes[0].startedAt).valueOf(); + const timelineEnd = nodes.map(node => moment(node.finishedAt).valueOf()).reduce((first, second) => Math.max(first, second), moment(timelineStart).valueOf()); + + function timeToLeft(time: number) { + return ((time - timelineStart) / (timelineEnd - timelineStart)) * Math.max(parentWidth, MIN_WIDTH) + NODE_NAME_WIDTH; } - private ensureRunningWorkflowRefreshing(workflow: models.Workflow) { - const completedPhases = [models.NODE_PHASE.ERROR, models.NODE_PHASE.SUCCEEDED, models.NODE_PHASE.SKIPPED, models.NODE_PHASE.OMITTED, models.NODE_PHASE.FAILED]; - const isCompleted = workflow && workflow.status && completedPhases.indexOf(workflow.status.phase) > -1; - if (!this.refreshSubscription && !isCompleted) { - this.refreshSubscription = interval(1000).subscribe(() => { - this.setState({now: moment()}); - }); - } else if (this.refreshSubscription && isCompleted) { - this.refreshSubscription.unsubscribe(); - this.refreshSubscription = null; + const groups = nodes.map(node => ({ + startedAt: moment(node.startedAt).valueOf(), + finishedAt: moment(node.finishedAt).valueOf(), + nodes: [ + Object.assign({}, node, { + left: timeToLeft(moment(node.startedAt).valueOf()), + width: timeToLeft(moment(node.finishedAt).valueOf()) - timeToLeft(moment(node.startedAt).valueOf()) + }) + ] + })); + + for (let i = groups.length - 1; i >= 1; i--) { + const cur = groups[i]; + const next = groups[i - 1]; + if (moment(cur.startedAt).diff(next.finishedAt, 'milliseconds') < 0 && moment(next.startedAt).diff(cur.startedAt, 'milliseconds') < ROUND_START_DIFF_MS) { + next.nodes = next.nodes.concat(cur.nodes); + next.finishedAt = nodes.map(node => moment(node.finishedAt).valueOf()).reduce((first, second) => Math.max(first, second), next.finishedAt.valueOf()); + groups.splice(i, 1); } } + + return ( +
+
+
+ {groups.map(group => [ +
+ {moment(group.startedAt).format('hh:mm')} +
, + ...group.nodes.map(node => ( +
props.nodeClicked && props.nodeClicked(node)}> +
+ {Utils.shortNodeName(node)} +
+
+
+ )) + ])} +
+ ); }