diff --git a/ui/src/cron-workflows/cron-workflow-details.tsx b/ui/src/cron-workflows/cron-workflow-details.tsx index 566ccf766a4c..0d155cdcae52 100644 --- a/ui/src/cron-workflows/cron-workflow-details.tsx +++ b/ui/src/cron-workflows/cron-workflow-details.tsx @@ -2,7 +2,7 @@ import {NotificationType} from 'argo-ui/src/components/notifications/notificatio import {Page} from 'argo-ui/src/components/page/page'; import {SlidingPanel} from 'argo-ui/src/components/sliding-panel/sliding-panel'; import * as React from 'react'; -import {useContext, useEffect, useState} from 'react'; +import {useContext, useEffect, useRef, useState} from 'react'; import {RouteComponentProps} from 'react-router'; import {uiUrl} from '../shared/base'; @@ -29,6 +29,7 @@ export function CronWorkflowDetails({match, location, history}: RouteComponentPr const {navigation, notifications, popup} = useContext(Context); const queryParams = new URLSearchParams(location.search); + const isFirstRender = useRef(true); const [namespace] = useState(match.params.namespace); const [name] = useState(match.params.name); const [sidePanel, setSidePanel] = useState(queryParams.get('sidePanel')); @@ -47,18 +48,20 @@ export function CronWorkflowDetails({match, location, history}: RouteComponentPr [history] ); - useEffect( - () => - history.push( - historyUrl('cron-workflows/{namespace}/{name}', { - namespace, - name, - sidePanel, - tab - }) - ), - [namespace, name, sidePanel, tab] - ); + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + history.push( + historyUrl('cron-workflows/{namespace}/{name}', { + namespace, + name, + sidePanel, + tab + }) + ); + }, [namespace, name, sidePanel, tab]); useEffect(() => { services.cronWorkflows diff --git a/ui/src/cron-workflows/cron-workflow-list.tsx b/ui/src/cron-workflows/cron-workflow-list.tsx index dbbd6ba27967..0b4521729adf 100644 --- a/ui/src/cron-workflows/cron-workflow-list.tsx +++ b/ui/src/cron-workflows/cron-workflow-list.tsx @@ -1,7 +1,7 @@ import {Page} from 'argo-ui/src/components/page/page'; import {SlidingPanel} from 'argo-ui/src/components/sliding-panel/sliding-panel'; import * as React from 'react'; -import {useContext, useEffect, useState} from 'react'; +import {useContext, useEffect, useRef, useState} from 'react'; import {RouteComponentProps} from 'react-router-dom'; import {uiUrl} from '../shared/base'; @@ -33,6 +33,7 @@ export function CronWorkflowList({match, location, history}: RouteComponentProps const {navigation} = useContext(Context); // state for URL, query, and label parameters + const isFirstRender = useRef(true); const [namespace, setNamespace] = useState(nsUtils.getNamespace(match.params.namespace) || ''); const [sidePanel, setSidePanel] = useState(queryParams.get('sidePanel') === 'true'); const [labels, setLabels] = useState([]); @@ -49,16 +50,18 @@ export function CronWorkflowList({match, location, history}: RouteComponentProps ); // save history - useEffect( - () => - history.push( - historyUrl('cron-workflows' + (nsUtils.getManagedNamespace() ? '' : '/{namespace}'), { - namespace, - sidePanel - }) - ), - [namespace, sidePanel] - ); + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + history.push( + historyUrl('cron-workflows' + (nsUtils.getManagedNamespace() ? '' : '/{namespace}'), { + namespace, + sidePanel + }) + ); + }, [namespace, sidePanel]); // internal state const [error, setError] = useState(); diff --git a/ui/src/event-flow/event-flow-page.tsx b/ui/src/event-flow/event-flow-page.tsx index bdbb07e9cf29..8746bba00de3 100644 --- a/ui/src/event-flow/event-flow-page.tsx +++ b/ui/src/event-flow/event-flow-page.tsx @@ -1,7 +1,7 @@ import {Page} from 'argo-ui/src/components/page/page'; import {SlidingPanel} from 'argo-ui/src/components/sliding-panel/sliding-panel'; import {Tabs} from 'argo-ui/src/components/tabs/tabs'; -import {useContext, useEffect, useState} from 'react'; +import {useContext, useEffect, useRef, useState} from 'react'; import * as React from 'react'; import {RouteComponentProps} from 'react-router-dom'; import {Observable} from 'rxjs'; @@ -43,6 +43,7 @@ export function EventFlowPage({history, location, match}: RouteComponentProps - history.push( - historyUrl('event-flow' + (nsUtils.getManagedNamespace() ? '' : '/{namespace}'), { - namespace, - showFlow, - showWorkflows, - expanded, - selectedNode, - tab - }) - ), - [namespace, showFlow, showWorkflows, expanded, expanded, tab] - ); + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + history.push( + historyUrl('event-flow' + (nsUtils.getManagedNamespace() ? '' : '/{namespace}'), { + namespace, + showFlow, + showWorkflows, + expanded, + selectedNode, + tab + }) + ); + }, [namespace, showFlow, showWorkflows, expanded, expanded, tab]); // internal state const [error, setError] = useState(); diff --git a/ui/src/event-sources/event-source-details.tsx b/ui/src/event-sources/event-source-details.tsx index 0b6cbc1a17a0..6653246cba6e 100644 --- a/ui/src/event-sources/event-source-details.tsx +++ b/ui/src/event-sources/event-source-details.tsx @@ -3,7 +3,7 @@ import {Page} from 'argo-ui/src/components/page/page'; import {SlidingPanel} from 'argo-ui/src/components/sliding-panel/sliding-panel'; import {Tabs} from 'argo-ui/src/components/tabs/tabs'; import * as React from 'react'; -import {useContext, useEffect, useState} from 'react'; +import {useContext, useEffect, useRef, useState} from 'react'; import {RouteComponentProps} from 'react-router'; import {ID} from '../event-flow/id'; @@ -27,6 +27,7 @@ export function EventSourceDetails({history, location, match}: RouteComponentPro const queryParams = new URLSearchParams(location.search); // state for URL and query parameters + const isFirstRender = useRef(true); const namespace = match.params.namespace; const name = match.params.name; const [tab, setTab] = useState(queryParams.get('tab')); @@ -40,18 +41,20 @@ export function EventSourceDetails({history, location, match}: RouteComponentPro [history] ); - useEffect( - () => - history.push( - historyUrl('event-sources/{namespace}/{name}', { - namespace, - name, - tab, - selectedNode - }) - ), - [namespace, name, tab, selectedNode] - ); + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + history.push( + historyUrl('event-sources/{namespace}/{name}', { + namespace, + name, + tab, + selectedNode + }) + ); + }, [namespace, name, tab, selectedNode]); const [error, setError] = useState(); const {object: eventSource, setObject: setEventSource, resetObject: resetEventSource, serialization, edited, lang, setLang} = useEditableObject(); diff --git a/ui/src/event-sources/event-source-list.tsx b/ui/src/event-sources/event-source-list.tsx index 3548d25e68ec..516d9490cac3 100644 --- a/ui/src/event-sources/event-source-list.tsx +++ b/ui/src/event-sources/event-source-list.tsx @@ -3,7 +3,7 @@ import {SlidingPanel} from 'argo-ui/src/components/sliding-panel/sliding-panel'; import {Tabs} from 'argo-ui/src/components/tabs/tabs'; import classNames from 'classnames'; import * as React from 'react'; -import {useContext, useEffect, useState} from 'react'; +import {useContext, useEffect, useRef, useState} from 'react'; import {Link, RouteComponentProps} from 'react-router-dom'; import {ID} from '../event-flow/id'; @@ -36,6 +36,7 @@ export function EventSourceList({match, location, history}: RouteComponentProps< const {navigation} = useContext(Context); // state for URL and query parameters + const isFirstRender = useRef(true); const [namespace, setNamespace] = useState(nsUtils.getNamespace(match.params.namespace) || ''); const [sidePanel, setSidePanel] = useState(queryParams.get('sidePanel') === 'true'); const [selectedNode, setSelectedNode] = useState(queryParams.get('selectedNode')); @@ -50,18 +51,20 @@ export function EventSourceList({match, location, history}: RouteComponentProps< [history] ); - useEffect( - () => - history.push( - historyUrl('event-sources' + (nsUtils.getManagedNamespace() ? '' : '/{namespace}'), { - namespace, - sidePanel, - selectedNode, - tab - }) - ), - [namespace, sidePanel, selectedNode, tab] - ); + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + history.push( + historyUrl('event-sources' + (nsUtils.getManagedNamespace() ? '' : '/{namespace}'), { + namespace, + sidePanel, + selectedNode, + tab + }) + ); + }, [namespace, sidePanel, selectedNode, tab]); // internal state const [error, setError] = useState(); diff --git a/ui/src/plugins/plugin-list.tsx b/ui/src/plugins/plugin-list.tsx index c2069143f264..45cf9bbe57f3 100644 --- a/ui/src/plugins/plugin-list.tsx +++ b/ui/src/plugins/plugin-list.tsx @@ -1,6 +1,6 @@ import {Page} from 'argo-ui/src/components/page/page'; import * as React from 'react'; -import {useEffect, useState} from 'react'; +import {useEffect, useRef, useState} from 'react'; import {RouteComponentProps} from 'react-router-dom'; import {uiUrl} from '../shared/base'; @@ -11,16 +11,19 @@ import {useCollectEvent} from '../shared/use-collect-event'; export function PluginList({match, history}: RouteComponentProps) { // state for URL and query parameters + const isFirstRender = useRef(true); const [namespace] = useState(nsUtils.getNamespace(match.params.namespace) || ''); - useEffect( - () => - history.push( - historyUrl('plugins' + (nsUtils.getManagedNamespace() ? '' : '/{namespace}'), { - namespace - }) - ), - [namespace] - ); + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + history.push( + historyUrl('plugins' + (nsUtils.getManagedNamespace() ? '' : '/{namespace}'), { + namespace + }) + ); + }, [namespace]); useCollectEvent('openedPlugins'); return ( diff --git a/ui/src/reports/reports.tsx b/ui/src/reports/reports.tsx index f519a1f3ca20..543da810f8b4 100644 --- a/ui/src/reports/reports.tsx +++ b/ui/src/reports/reports.tsx @@ -4,7 +4,7 @@ import {ChartOptions} from 'chart.js'; import 'chartjs-plugin-annotation'; import * as React from 'react'; -import {useContext, useEffect, useState} from 'react'; +import {useContext, useEffect, useRef, useState} from 'react'; import {Bar, ChartData} from 'react-chartjs-2'; import {RouteComponentProps} from 'react-router-dom'; @@ -33,6 +33,7 @@ export function Reports({match, location, history}: RouteComponentProps) { const {navigation} = useContext(Context); // state for URL, query, and label parameters + const isFirstRender = useRef(true); const [namespace, setNamespace] = useState(nsUtils.getNamespace(match.params.namespace) || ''); const [labels, setLabels] = useState((queryParams.get('labels') || '').split(',').filter(v => v !== '')); // internal state @@ -41,6 +42,10 @@ export function Reports({match, location, history}: RouteComponentProps) { // save history useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } history.push(historyUrl('reports' + (nsUtils.getManagedNamespace() ? '' : '/{namespace}'), {namespace, labels: labels.join(',')})); }, [namespace, labels]); diff --git a/ui/src/sensors/sensor-details.tsx b/ui/src/sensors/sensor-details.tsx index e8919dc113ee..7af081b9ac4b 100644 --- a/ui/src/sensors/sensor-details.tsx +++ b/ui/src/sensors/sensor-details.tsx @@ -1,7 +1,7 @@ import {NotificationType} from 'argo-ui/src/components/notifications/notifications'; import {Page} from 'argo-ui/src/components/page/page'; import * as React from 'react'; -import {useContext, useEffect, useState} from 'react'; +import {useContext, useEffect, useRef, useState} from 'react'; import {RouteComponentProps} from 'react-router'; import {ID} from '../event-flow/id'; @@ -23,6 +23,7 @@ import '../workflows/components/workflow-details/workflow-details.scss'; export function SensorDetails({match, location, history}: RouteComponentProps) { // boiler-plate + const isFirstRender = useRef(true); const {navigation, notifications, popup} = useContext(Context); const queryParams = new URLSearchParams(location.search); @@ -42,18 +43,20 @@ export function SensorDetails({match, location, history}: RouteComponentProps - history.push( - historyUrl('sensors/{namespace}/{name}', { - namespace, - name, - tab, - selectedLogNode - }) - ), - [namespace, name, tab, selectedLogNode] - ); + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + history.push( + historyUrl('sensors/{namespace}/{name}', { + namespace, + name, + tab, + selectedLogNode + }) + ); + }, [namespace, name, tab, selectedLogNode]); useEffect(() => { services.sensor diff --git a/ui/src/sensors/sensor-list.tsx b/ui/src/sensors/sensor-list.tsx index 98ff6531550f..fd2c5152367a 100644 --- a/ui/src/sensors/sensor-list.tsx +++ b/ui/src/sensors/sensor-list.tsx @@ -2,7 +2,7 @@ import {Page} from 'argo-ui/src/components/page/page'; import {SlidingPanel} from 'argo-ui/src/components/sliding-panel/sliding-panel'; import classNames from 'classnames'; import * as React from 'react'; -import {useContext, useEffect, useState} from 'react'; +import {useContext, useEffect, useRef, useState} from 'react'; import {Link, RouteComponentProps} from 'react-router-dom'; import {ID} from '../event-flow/id'; @@ -34,6 +34,7 @@ export function SensorList({match, location, history}: RouteComponentProps) const {navigation} = useContext(Context); // state for URL and query parameters + const isFirstRender = useRef(true); const [namespace, setNamespace] = useState(nsUtils.getNamespace(match.params.namespace) || ''); const [sidePanel, setSidePanel] = useState(queryParams.get('sidePanel') === 'true'); const [selectedNode, setSelectedNode] = useState(queryParams.get('selectedNode')); @@ -46,17 +47,19 @@ export function SensorList({match, location, history}: RouteComponentProps) [history] ); - useEffect( - () => - history.push( - historyUrl('sensors' + (nsUtils.getManagedNamespace() ? '' : '/{namespace}'), { - namespace, - sidePanel, - selectedNode - }) - ), - [namespace, sidePanel, selectedNode] - ); + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + history.push( + historyUrl('sensors' + (nsUtils.getManagedNamespace() ? '' : '/{namespace}'), { + namespace, + sidePanel, + selectedNode + }) + ); + }, [namespace, sidePanel, selectedNode]); // internal state const [error, setError] = useState(); diff --git a/ui/src/widgets/workflow-graph.tsx b/ui/src/widgets/workflow-graph.tsx index c38b6546d94f..7ab82005f494 100644 --- a/ui/src/widgets/workflow-graph.tsx +++ b/ui/src/widgets/workflow-graph.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import {useEffect, useState} from 'react'; +import {useEffect, useRef, useState} from 'react'; import {RouteComponentProps} from 'react-router'; import {uiUrl} from '../shared/base'; @@ -11,6 +11,7 @@ import {services} from '../shared/services'; import {WorkflowDag} from '../workflows/components/workflow-dag/workflow-dag'; export function WorkflowGraph({history, match}: RouteComponentProps) { + const isFirstRender = useRef(true); const queryParams = new URLSearchParams(location.search); const namespace = match.params.namespace; const name = queryParams.get('name'); @@ -20,6 +21,10 @@ export function WorkflowGraph({history, match}: RouteComponentProps) { const target = queryParams.get('target') || '_top'; useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } history.push( historyUrl('widgets/workflow-graphs/{namespace}', { namespace, diff --git a/ui/src/widgets/workflow-status-badge.tsx b/ui/src/widgets/workflow-status-badge.tsx index 50da09b80a51..6753198800a5 100644 --- a/ui/src/widgets/workflow-status-badge.tsx +++ b/ui/src/widgets/workflow-status-badge.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import {useEffect, useState} from 'react'; +import {useEffect, useRef, useState} from 'react'; import {RouteComponentProps} from 'react-router'; import {uiUrl} from '../shared/base'; @@ -11,6 +11,7 @@ import {services} from '../shared/services'; import './workflow-status-badge.scss'; export function WorkflowStatusBadge({history, match}: RouteComponentProps) { + const isFirstRender = useRef(true); const queryParams = new URLSearchParams(location.search); const namespace = match.params.namespace; const name = queryParams.get('name'); @@ -26,6 +27,10 @@ export function WorkflowStatusBadge({history, match}: RouteComponentProps) const [phase, setPhase] = useState(''); useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } const w = new RetryWatch( () => services.workflows.watch({namespace, name, labels: [label]}), () => setDisplayName(null), diff --git a/ui/src/workflow-event-bindings/workflow-event-bindings.tsx b/ui/src/workflow-event-bindings/workflow-event-bindings.tsx index b3556c41bfca..9fd43c245289 100644 --- a/ui/src/workflow-event-bindings/workflow-event-bindings.tsx +++ b/ui/src/workflow-event-bindings/workflow-event-bindings.tsx @@ -1,7 +1,7 @@ import {Page} from 'argo-ui/src/components/page/page'; import {SlidingPanel} from 'argo-ui/src/components/sliding-panel/sliding-panel'; import * as React from 'react'; -import {useContext, useEffect, useState} from 'react'; +import {useContext, useEffect, useRef, useState} from 'react'; import {RouteComponentProps} from 'react-router-dom'; import {absoluteUrl, uiUrl} from '../shared/base'; @@ -33,6 +33,7 @@ const learnMore = ) { // boiler-plate + const isFirstRender = useRef(true); const ctx = useContext(Context); const queryParams = new URLSearchParams(location.search); @@ -47,16 +48,18 @@ export function WorkflowEventBindings({match, location, history}: RouteComponent [history] ); - useEffect( - () => - history.push( - historyUrl('workflow-event-bindings' + (nsUtils.getManagedNamespace() ? '' : '/{namespace}'), { - namespace, - selectedWorkflowEventBinding - }) - ), - [namespace, selectedWorkflowEventBinding] - ); + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + history.push( + historyUrl('workflow-event-bindings' + (nsUtils.getManagedNamespace() ? '' : '/{namespace}'), { + namespace, + selectedWorkflowEventBinding + }) + ); + }, [namespace, selectedWorkflowEventBinding]); // internal state const [error, setError] = useState(); diff --git a/ui/src/workflow-templates/workflow-template-details.tsx b/ui/src/workflow-templates/workflow-template-details.tsx index aca250a64449..603c53f575e9 100644 --- a/ui/src/workflow-templates/workflow-template-details.tsx +++ b/ui/src/workflow-templates/workflow-template-details.tsx @@ -2,7 +2,7 @@ import {NotificationType} from 'argo-ui/src/components/notifications/notificatio import {Page} from 'argo-ui/src/components/page/page'; import {SlidingPanel} from 'argo-ui/src/components/sliding-panel/sliding-panel'; import * as React from 'react'; -import {useContext, useEffect, useState} from 'react'; +import {useContext, useEffect, useRef, useState} from 'react'; import {RouteComponentProps} from 'react-router'; import {uiUrl} from '../shared/base'; @@ -28,6 +28,7 @@ export function WorkflowTemplateDetails({history, location, match}: RouteCompone const queryParams = new URLSearchParams(location.search); // state for URL and query parameters + const isFirstRender = useRef(true); const namespace = match.params.namespace; const name = match.params.name; const [sidePanel, setSidePanel] = useState(queryParams.get('sidePanel')); @@ -45,18 +46,20 @@ export function WorkflowTemplateDetails({history, location, match}: RouteCompone [history] ); - useEffect( - () => - history.push( - historyUrl('workflow-templates/{namespace}/{name}', { - namespace, - name, - sidePanel, - tab - }) - ), - [namespace, name, sidePanel, tab] - ); + useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + history.push( + historyUrl('workflow-templates/{namespace}/{name}', { + namespace, + name, + sidePanel, + tab + }) + ); + }, [namespace, name, sidePanel, tab]); useEffect(() => { services.workflowTemplate diff --git a/ui/src/workflow-templates/workflow-template-list.tsx b/ui/src/workflow-templates/workflow-template-list.tsx index b7d4c1e23b98..26dc9df01637 100644 --- a/ui/src/workflow-templates/workflow-template-list.tsx +++ b/ui/src/workflow-templates/workflow-template-list.tsx @@ -1,7 +1,7 @@ import {Page} from 'argo-ui/src/components/page/page'; import {SlidingPanel} from 'argo-ui/src/components/sliding-panel/sliding-panel'; import * as React from 'react'; -import {useContext, useEffect, useState} from 'react'; +import {useContext, useEffect, useRef, useState} from 'react'; import {RouteComponentProps} from 'react-router-dom'; import {uiUrl} from '../shared/base'; @@ -33,6 +33,7 @@ const learnMore = { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } + history.push( + historyUrl('workflow-templates' + (nsUtils.getManagedNamespace() ? '' : '/{namespace}'), { + namespace, + sidePanel + }) + ); + }, [namespace, sidePanel]); // internal state const [error, setError] = useState(); diff --git a/ui/src/workflows/components/workflow-details/workflow-details.tsx b/ui/src/workflows/components/workflow-details/workflow-details.tsx index f48bbac10498..5280e0ed2270 100644 --- a/ui/src/workflows/components/workflow-details/workflow-details.tsx +++ b/ui/src/workflows/components/workflow-details/workflow-details.tsx @@ -98,6 +98,7 @@ export function WorkflowDetails({history, location, match}: RouteComponentProps< const namespace = match.params.namespace; const name = match.params.name; + const isFirstRender = useRef(true); const [tab, setTab] = useState(queryParams.get('tab') || 'workflow'); const [uid, setUid] = useState(queryParams.get('uid') || ''); const [nodeId, setNodeId] = useState(queryParams.get('nodeId')); @@ -163,6 +164,10 @@ export function WorkflowDetails({history, location, match}: RouteComponentProps< }, [workflow, selectedArtifact]); useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } history.push(historyUrl('workflows/{namespace}/{name}', {namespace, name, tab, nodeId, nodePanelView, sidePanel, uid})); }, [namespace, name, tab, nodeId, nodePanelView, sidePanel, uid]); diff --git a/ui/src/workflows/components/workflows-list/workflows-list.tsx b/ui/src/workflows/components/workflows-list/workflows-list.tsx index 00d57401547a..23e574c29f99 100644 --- a/ui/src/workflows/components/workflows-list/workflows-list.tsx +++ b/ui/src/workflows/components/workflows-list/workflows-list.tsx @@ -1,7 +1,7 @@ import {Page} from 'argo-ui/src/components/page/page'; import {SlidingPanel} from 'argo-ui/src/components/sliding-panel/sliding-panel'; import * as React from 'react'; -import {useContext, useEffect, useMemo, useState} from 'react'; +import {useContext, useEffect, useMemo, useRef, useState} from 'react'; import {RouteComponentProps} from 'react-router-dom'; import {uiUrl} from '../../../shared/base'; @@ -58,6 +58,7 @@ export function WorkflowsList({match, location, history}: RouteComponentProps(() => { const savedPaginationLimit = storage.getItem('options', {}).paginationLimit || undefined; @@ -126,6 +127,10 @@ export function WorkflowsList({match, location, history}: RouteComponentProps { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } // add empty selectedPhases + selectedLabels for forward-compat w/ old version: previous code relies on them existing, so if you move up a version and back down, it breaks const options = {selectedPhases: [], selectedLabels: []} as unknown as WorkflowListRenderOptions; options.phases = phases;