diff --git a/persist/sqldb/workflow_archive.go b/persist/sqldb/workflow_archive.go index 35251f1ac3fe..15c780071b29 100644 --- a/persist/sqldb/workflow_archive.go +++ b/persist/sqldb/workflow_archive.go @@ -8,9 +8,7 @@ import ( log "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/types" "upper.io/db.v3" "upper.io/db.v3/lib/sqlbuilder" @@ -144,7 +142,7 @@ func (r *workflowArchive) ArchiveWorkflow(wf *wfv1.Workflow) error { } func (r *workflowArchive) ListWorkflows(namespace string, name string, namePrefix string, minStartedAt, maxStartedAt time.Time, labelRequirements labels.Requirements, limit int, offset int) (wfv1.Workflows, error) { - var archivedWfs []archivedWorkflowMetadata + var archivedWfs []archivedWorkflowRecord clause, err := labelsClause(r.dbType, labelRequirements) if err != nil { return nil, err @@ -158,7 +156,7 @@ func (r *workflowArchive) ListWorkflows(namespace string, name string, namePrefi } err = r.session. - Select("name", "namespace", "uid", "phase", "startedat", "finishedat"). + Select("workflow"). From(archiveTableName). Where(r.clusterManagedNamespaceAndInstanceID()). And(namespaceEqual(namespace)). @@ -173,20 +171,14 @@ func (r *workflowArchive) ListWorkflows(namespace string, name string, namePrefi if err != nil { return nil, err } - wfs := make(wfv1.Workflows, len(archivedWfs)) - for i, md := range archivedWfs { - wfs[i] = wfv1.Workflow{ - ObjectMeta: v1.ObjectMeta{ - Name: md.Name, - Namespace: md.Namespace, - UID: types.UID(md.UID), - CreationTimestamp: v1.Time{Time: md.StartedAt}, - }, - Status: wfv1.WorkflowStatus{ - Phase: md.Phase, - StartedAt: v1.Time{Time: md.StartedAt}, - FinishedAt: v1.Time{Time: md.FinishedAt}, - }, + wfs := make(wfv1.Workflows, 0) + for _, archivedWf := range archivedWfs { + wf := wfv1.Workflow{} + err = json.Unmarshal([]byte(archivedWf.Workflow), &wf) + if err != nil { + log.WithFields(log.Fields{"workflowUID": archivedWf.UID, "workflowName": archivedWf.Name}).Errorln("unable to unmarshal workflow from database") + } else { + wfs = append(wfs, wf) } } return wfs, nil diff --git a/server/workflow/workflow_server.go b/server/workflow/workflow_server.go index b465e411f97e..dab707dc4b76 100644 --- a/server/workflow/workflow_server.go +++ b/server/workflow/workflow_server.go @@ -20,6 +20,7 @@ import ( workflowpkg "github.com/argoproj/argo-workflows/v3/pkg/apiclient/workflow" workflowarchivepkg "github.com/argoproj/argo-workflows/v3/pkg/apiclient/workflowarchive" "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow" + "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" wfv1 "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" "github.com/argoproj/argo-workflows/v3/pkg/client/clientset/versioned" "github.com/argoproj/argo-workflows/v3/server/auth" @@ -128,6 +129,32 @@ func (s *workflowServer) GetWorkflow(ctx context.Context, req *workflowpkg.Workf return wf, nil } +func mergeWithArchivedWorkflows(liveWfs v1alpha1.WorkflowList, archivedWfs v1alpha1.WorkflowList, numWfsToKeep int) *v1alpha1.WorkflowList { + var mergedWfs []v1alpha1.Workflow + var uidToWfs = map[types.UID][]v1alpha1.Workflow{} + for _, item := range liveWfs.Items { + uidToWfs[item.UID] = append(uidToWfs[item.UID], item) + } + for _, item := range archivedWfs.Items { + uidToWfs[item.UID] = append(uidToWfs[item.UID], item) + } + + for _, v := range uidToWfs { + mergedWfs = append(mergedWfs, v[0]) + } + mergedWfsList := v1alpha1.WorkflowList{Items: mergedWfs, ListMeta: liveWfs.ListMeta} + sort.Sort(mergedWfsList.Items) + numWfs := 0 + var finalWfs []v1alpha1.Workflow + for _, item := range mergedWfsList.Items { + if numWfsToKeep == 0 || numWfs < numWfsToKeep { + finalWfs = append(finalWfs, item) + numWfs += 1 + } + } + return &v1alpha1.WorkflowList{Items: finalWfs, ListMeta: liveWfs.ListMeta} +} + func (s *workflowServer) ListWorkflows(ctx context.Context, req *workflowpkg.WorkflowListRequest) (*wfv1.WorkflowList, error) { wfClient := auth.GetWfClient(ctx) @@ -140,6 +167,19 @@ func (s *workflowServer) ListWorkflows(ctx context.Context, req *workflowpkg.Wor if err != nil { return nil, sutils.ToStatusError(err, codes.Internal) } + archivedWfList, err := s.wfArchiveServer.ListArchivedWorkflows(ctx, &workflowarchivepkg.ListArchivedWorkflowsRequest{ + ListOptions: listOption, + NamePrefix: "", + Namespace: req.Namespace, + }) + if err != nil { + log.Warnf("unable to list archived workflows:%v", err) + } else { + if archivedWfList != nil { + wfList = mergeWithArchivedWorkflows(*wfList, *archivedWfList, int(listOption.Limit)) + } + } + cleaner := fields.NewCleaner(req.Fields) if s.offloadNodeStatusRepo.IsEnabled() && !cleaner.WillExclude("items.status.nodes") { offloadedNodes, err := s.offloadNodeStatusRepo.List(req.Namespace) diff --git a/server/workflow/workflow_server_test.go b/server/workflow/workflow_server_test.go index 1d17bba26bca..f747f8f42997 100644 --- a/server/workflow/workflow_server_test.go +++ b/server/workflow/workflow_server_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "testing" + "time" "github.com/go-jose/go-jose/v3/jwt" "github.com/stretchr/testify/assert" @@ -648,6 +649,22 @@ func (t testWatchWorkflowServer) Send(*workflowpkg.WorkflowWatchEvent) error { panic("implement me") } +func TestMergeWithArchivedWorkflows(t *testing.T) { + timeNow := time.Now() + wf1 := v1alpha1.Workflow{ + ObjectMeta: metav1.ObjectMeta{UID: "1", CreationTimestamp: metav1.Time{Time: timeNow.Add(time.Second)}}} + wf2 := v1alpha1.Workflow{ + ObjectMeta: metav1.ObjectMeta{UID: "2", CreationTimestamp: metav1.Time{Time: timeNow.Add(2 * time.Second)}}} + wf3 := v1alpha1.Workflow{ + ObjectMeta: metav1.ObjectMeta{UID: "3", CreationTimestamp: metav1.Time{Time: timeNow.Add(3 * time.Second)}}} + liveWfList := v1alpha1.WorkflowList{Items: []v1alpha1.Workflow{wf1, wf2}} + archivedWfList := v1alpha1.WorkflowList{Items: []v1alpha1.Workflow{wf1, wf3, wf2}} + expectedWfList := v1alpha1.WorkflowList{Items: []v1alpha1.Workflow{wf3, wf2, wf1}} + expectedShortWfList := v1alpha1.WorkflowList{Items: []v1alpha1.Workflow{wf3, wf2}} + assert.Equal(t, expectedWfList.Items, mergeWithArchivedWorkflows(liveWfList, archivedWfList, 0).Items) + assert.Equal(t, expectedShortWfList.Items, mergeWithArchivedWorkflows(liveWfList, archivedWfList, 2).Items) +} + func TestWatchWorkflows(t *testing.T) { server, ctx := getWorkflowServer() wf := &v1alpha1.Workflow{ diff --git a/ui/src/app/app-router.tsx b/ui/src/app/app-router.tsx index 3db7e2d811d0..52c8933681c1 100644 --- a/ui/src/app/app-router.tsx +++ b/ui/src/app/app-router.tsx @@ -6,7 +6,6 @@ import {useEffect, useState} from 'react'; import {Redirect, Route, Router, Switch} from 'react-router'; import {Version} from '../models'; import apidocs from './apidocs'; -import archivedWorkflows from './archived-workflows'; import clusterWorkflowTemplates from './cluster-workflow-templates'; import cronWorkflows from './cron-workflows'; import eventflow from './event-flow'; @@ -35,7 +34,6 @@ const workflowsEventBindingsUrl = uiUrl('workflow-event-bindings'); const workflowTemplatesUrl = uiUrl('workflow-templates'); const clusterWorkflowTemplatesUrl = uiUrl('cluster-workflow-templates'); const cronWorkflowsUrl = uiUrl('cron-workflows'); -const archivedWorkflowsUrl = uiUrl('archived-workflows'); const eventSourceUrl = uiUrl('event-sources'); const pluginsUrl = uiUrl('plugins'); const helpUrl = uiUrl('help'); @@ -130,11 +128,6 @@ export const AppRouter = ({popupManager, history, notificationsManager}: {popupM path: workflowsEventBindingsUrl + namespaceSuffix, iconClassName: 'fa fa-link' }, - { - title: 'Archived Workflows', - path: archivedWorkflowsUrl + namespaceSuffix, - iconClassName: 'fa fa-archive' - }, { title: 'Reports', path: reportsUrl + namespaceSuffix, @@ -176,7 +169,6 @@ export const AppRouter = ({popupManager, history, notificationsManager}: {popupM - diff --git a/ui/src/app/archived-workflows/components/archived-workflow-container.tsx b/ui/src/app/archived-workflows/components/archived-workflow-container.tsx deleted file mode 100644 index 5b7b63e7e012..000000000000 --- a/ui/src/app/archived-workflows/components/archived-workflow-container.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import * as React from 'react'; -import {Route, RouteComponentProps, Switch} from 'react-router'; -import {ArchivedWorkflowDetails} from './archived-workflow-details/archived-workflow-details'; -import {ArchivedWorkflowList} from './archived-workflow-list/archived-workflow-list'; - -export const ArchivedWorkflowContainer = (props: RouteComponentProps) => ( - - - - -); diff --git a/ui/src/app/archived-workflows/components/archived-workflow-details/archived-workflow-details.tsx b/ui/src/app/archived-workflows/components/archived-workflow-details/archived-workflow-details.tsx deleted file mode 100644 index d88a98b6646d..000000000000 --- a/ui/src/app/archived-workflows/components/archived-workflow-details/archived-workflow-details.tsx +++ /dev/null @@ -1,315 +0,0 @@ -import {NotificationType, Page, SlidingPanel} from 'argo-ui'; -import * as classNames from 'classnames'; -import * as React from 'react'; -import {useContext, useEffect, useState} from 'react'; -import {RouteComponentProps} from 'react-router'; -import {ArtifactRepository, execSpec, Link, Workflow} from '../../../../models'; -import {artifactRepoHasLocation, findArtifact} from '../../../shared/artifacts'; -import {uiUrl} from '../../../shared/base'; -import {ErrorNotice} from '../../../shared/components/error-notice'; -import {ProcessURL} from '../../../shared/components/links'; -import {Loading} from '../../../shared/components/loading'; -import {Context} from '../../../shared/context'; -import {services} from '../../../shared/services'; -import {useQueryParams} from '../../../shared/use-query-params'; -import {WorkflowArtifacts} from '../../../workflows/components/workflow-artifacts'; - -import {ANNOTATION_KEY_POD_NAME_VERSION} from '../../../shared/annotations'; -import {getPodName, getTemplateNameFromNode} from '../../../shared/pod-name'; -import {getResolvedTemplates} from '../../../shared/template-resolution'; -import {ArtifactPanel} from '../../../workflows/components/workflow-details/artifact-panel'; -import {WorkflowResourcePanel} from '../../../workflows/components/workflow-details/workflow-resource-panel'; -import {WorkflowLogsViewer} from '../../../workflows/components/workflow-logs-viewer/workflow-logs-viewer'; -import {WorkflowNodeInfo} from '../../../workflows/components/workflow-node-info/workflow-node-info'; -import {WorkflowPanel} from '../../../workflows/components/workflow-panel/workflow-panel'; -import {WorkflowParametersPanel} from '../../../workflows/components/workflow-parameters-panel'; -import {WorkflowSummaryPanel} from '../../../workflows/components/workflow-summary-panel'; -import {WorkflowTimeline} from '../../../workflows/components/workflow-timeline/workflow-timeline'; -import {WorkflowYamlViewer} from '../../../workflows/components/workflow-yaml-viewer/workflow-yaml-viewer'; - -require('../../../workflows/components/workflow-details/workflow-details.scss'); - -const STEP_GRAPH_CONTAINER_MIN_WIDTH = 490; -const STEP_INFO_WIDTH = 570; - -export const ArchivedWorkflowDetails = ({history, location, match}: RouteComponentProps) => { - const ctx = useContext(Context); - const queryParams = new URLSearchParams(location.search); - const [workflow, setWorkflow] = useState(); - const [links, setLinks] = useState(); - const [error, setError] = useState(); - - const [namespace] = useState(match.params.namespace); - const [uid] = useState(match.params.uid); - const [tab, setTab] = useState(queryParams.get('tab') || 'workflow'); - const [nodeId, setNodeId] = useState(queryParams.get('nodeId')); - const [container, setContainer] = useState(queryParams.get('container') || 'main'); - const [sidePanel, setSidePanel] = useState(queryParams.get('sidePanel')); - const selectedArtifact = workflow && workflow.status && findArtifact(workflow.status, nodeId); - const [selectedTemplateArtifactRepo, setSelectedTemplateArtifactRepo] = useState(); - const node = nodeId && workflow.status.nodes[nodeId]; - - const podName = () => { - if (nodeId && workflow) { - const workflowName = workflow.metadata.name; - const annotations = workflow.metadata.annotations || {}; - const version = annotations[ANNOTATION_KEY_POD_NAME_VERSION]; - const templateName = getTemplateNameFromNode(node); - return getPodName(workflowName, node.name, templateName, nodeId, version); - } - }; - - useEffect( - useQueryParams(history, p => { - setSidePanel(p.get('sidePanel')); - setNodeId(p.get('nodeId')); - setContainer(p.get('container')); - }), - [history] - ); - - useEffect(() => { - services.info - .getInfo() - .then(info => setLinks(info.links)) - .then(() => - services.archivedWorkflows.get(uid, namespace).then(retrievedWorkflow => { - setError(null); - setWorkflow(retrievedWorkflow); - }) - ) - .catch(newError => setError(newError)); - services.info.collectEvent('openedArchivedWorkflowDetails').then(); - }, []); - - useEffect(() => { - // update the default Artifact Repository for the Template that corresponds to the selectedArtifact - // if there's an ArtifactLocation configured for the Template we use that - // otherwise we use the central one for the Workflow configured in workflow.status.artifactRepositoryRef.artifactRepository - // (Note that individual Artifacts may also override whatever this gets set to) - if (workflow?.status?.nodes && selectedArtifact) { - const template = getResolvedTemplates(workflow, workflow.status.nodes[selectedArtifact.nodeId]); - const artifactRepo = template?.archiveLocation; - if (artifactRepo && artifactRepoHasLocation(artifactRepo)) { - setSelectedTemplateArtifactRepo(artifactRepo); - } else { - setSelectedTemplateArtifactRepo(workflow.status.artifactRepositoryRef.artifactRepository); - } - } - }, [workflow, selectedArtifact]); - - const renderArchivedWorkflowDetails = () => { - if (error) { - return ; - } - if (!workflow) { - return ; - } - return ( - <> - {tab === 'summary' ? ( -
-
-
- - {execSpec(workflow).arguments && execSpec(workflow).arguments.parameters && ( - -
Parameters
- -
- )} -
Artifacts
- - -
-
-
- ) : ( -
-
- {tab === 'workflow' ? ( - setNodeId(newNodeId)} - /> - ) : ( - setNodeId(newNode.id)} /> - )} -
- {nodeId && ( -
- - {node && ( - { - setSidePanel('yaml'); - setNodeId(newNodeId); - }} - onShowContainerLogs={(newNodeId, newContainer) => { - setSidePanel('logs'); - setNodeId(newNodeId); - setContainer(newContainer); - }} - archived={true} - /> - )} - {selectedArtifact && ( - - )} -
- )} -
- )} - setSidePanel(null)}> - {sidePanel === 'yaml' && } - {sidePanel === 'logs' && } - - - ); - }; - - const deleteArchivedWorkflow = () => { - if (!confirm('Are you sure you want to delete this archived workflow?\nThere is no undo.')) { - return; - } - services.archivedWorkflows - .delete(uid, workflow.metadata.namespace) - .then(() => { - document.location.href = uiUrl('archived-workflows'); - }) - .catch(e => { - ctx.notifications.show({ - content: 'Failed to delete archived workflow ' + e, - type: NotificationType.Error - }); - }); - }; - - const resubmitArchivedWorkflow = () => { - if (!confirm('Are you sure you want to resubmit this archived workflow?')) { - return; - } - services.archivedWorkflows - .resubmit(workflow.metadata.uid, workflow.metadata.namespace) - .then(newWorkflow => (document.location.href = uiUrl(`workflows/${newWorkflow.metadata.namespace}/${newWorkflow.metadata.name}`))) - .catch(e => { - ctx.notifications.show({ - content: 'Failed to resubmit archived workflow ' + e, - type: NotificationType.Error - }); - }); - }; - - const retryArchivedWorkflow = () => { - if (!confirm('Are you sure you want to retry this archived workflow?')) { - return; - } - services.archivedWorkflows - .retry(workflow.metadata.uid, workflow.metadata.namespace) - .then(newWorkflow => (document.location.href = uiUrl(`workflows/${newWorkflow.metadata.namespace}/${newWorkflow.metadata.name}`))) - .catch(e => { - ctx.notifications.show({ - content: 'Failed to retry archived workflow ' + e, - type: NotificationType.Error - }); - }); - }; - - const openLink = (link: Link) => { - const object = { - metadata: { - namespace: workflow.metadata.namespace, - name: workflow.metadata.name - }, - workflow, - status: { - startedAt: workflow.status.startedAt, - finishedAt: workflow.status.finishedAt - } - }; - const url = ProcessURL(link.url, object); - - if ((window.event as MouseEvent).ctrlKey || (window.event as MouseEvent).metaKey) { - window.open(url, '_blank'); - } else { - document.location.href = url; - } - }; - - const workflowPhase = workflow?.status?.phase; - const items = [ - { - title: 'Retry', - iconClassName: 'fa fa-undo', - disabled: workflowPhase === undefined || !(workflowPhase === 'Failed' || workflowPhase === 'Error'), - action: () => retryArchivedWorkflow() - }, - { - title: 'Resubmit', - iconClassName: 'fa fa-plus-circle', - disabled: false, - action: () => resubmitArchivedWorkflow() - }, - { - title: 'Delete', - iconClassName: 'fa fa-trash', - disabled: false, - action: () => deleteArchivedWorkflow() - } - ]; - if (links) { - links - .filter(link => link.scope === 'workflow') - .forEach(link => - items.push({ - title: link.name, - iconClassName: 'fa fa-external-link-alt', - disabled: false, - action: () => openLink(link) - }) - ); - } - - return ( - - setTab('summary')}> - - - setTab('timeline')}> - - - setTab('workflow')}> - - - - ) - }}> -
{renderArchivedWorkflowDetails()}
-
- ); -}; diff --git a/ui/src/app/archived-workflows/components/archived-workflow-filters/archived-workflow-filters.scss b/ui/src/app/archived-workflows/components/archived-workflow-filters/archived-workflow-filters.scss deleted file mode 100644 index 65f18b9410ca..000000000000 --- a/ui/src/app/archived-workflows/components/archived-workflow-filters/archived-workflow-filters.scss +++ /dev/null @@ -1,28 +0,0 @@ -@import 'node_modules/argo-ui/src/styles/config'; - -.wf-filters-container { - overflow: visible; - position: relative; - border-radius: 5px; - box-shadow: 1px 1px 3px #8fa4b1; - padding: 0 1em 0.75em 1em; - margin: 12px 0; - background-color: white; -} - -.wf-filters-container p { - margin: 0; - margin-top: 1em; - color: #6d7f8b; - text-transform: uppercase; -} - -.wf-filters-container__title { - position: relative; - width: 100%; - max-width: 100%; - padding: 8px 0; - font-size: 15px; - background-color: transparent; - border: 0; -} diff --git a/ui/src/app/archived-workflows/components/archived-workflow-filters/archived-workflow-filters.tsx b/ui/src/app/archived-workflows/components/archived-workflow-filters/archived-workflow-filters.tsx deleted file mode 100644 index 111c2a52d404..000000000000 --- a/ui/src/app/archived-workflows/components/archived-workflow-filters/archived-workflow-filters.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import * as React from 'react'; -import DatePicker from 'react-datepicker'; -import * as models from '../../../../models'; -import {CheckboxFilter} from '../../../shared/components/checkbox-filter/checkbox-filter'; -import {InputFilter} from '../../../shared/components/input-filter'; -import {NamespaceFilter} from '../../../shared/components/namespace-filter'; -import {TagsInput} from '../../../shared/components/tags-input/tags-input'; -import {services} from '../../../shared/services'; - -import 'react-datepicker/dist/react-datepicker.css'; - -require('./archived-workflow-filters.scss'); - -interface ArchivedWorkflowFilterProps { - workflows: models.Workflow[]; - namespace: string; - name: string; - namePrefix: string; - phaseItems: string[]; - selectedPhases: string[]; - selectedLabels: string[]; - minStartedAt?: Date; - maxStartedAt?: Date; - onChange: (namespace: string, name: string, namePrefix: string, selectedPhases: string[], labels: string[], minStartedAt: Date, maxStartedAt: Date) => void; -} - -interface State { - labels: string[]; -} - -export class ArchivedWorkflowFilters extends React.Component { - constructor(props: ArchivedWorkflowFilterProps) { - super(props); - this.state = { - labels: [] - }; - } - - public componentDidMount(): void { - this.fetchArchivedWorkflowsLabelKeys(); - } - - public render() { - return ( -
-
-
-

Namespace

- { - this.props.onChange( - ns, - this.props.name, - this.props.namePrefix, - this.props.selectedPhases, - this.props.selectedLabels, - this.props.minStartedAt, - this.props.maxStartedAt - ); - }} - /> -
-
-

Name

- { - this.props.onChange( - this.props.namespace, - wfname, - this.props.namePrefix, - this.props.selectedPhases, - this.props.selectedLabels, - this.props.minStartedAt, - this.props.maxStartedAt - ); - }} - /> -
-
-

Name Prefix

- { - this.props.onChange( - this.props.namespace, - this.props.name, - wfnamePrefix, - this.props.selectedPhases, - this.props.selectedLabels, - this.props.minStartedAt, - this.props.maxStartedAt - ); - }} - /> -
-
-

Labels

- { - this.props.onChange( - this.props.namespace, - this.props.name, - this.props.namePrefix, - this.props.selectedPhases, - tags, - this.props.minStartedAt, - this.props.maxStartedAt - ); - }} - /> -
-
-

Phases

- { - this.props.onChange( - this.props.namespace, - this.props.name, - this.props.namePrefix, - selected, - this.props.selectedLabels, - this.props.minStartedAt, - this.props.maxStartedAt - ); - }} - items={this.getPhaseItems(this.props.workflows)} - type='phase' - /> -
-
-

Started Time

- { - this.props.onChange( - this.props.namespace, - this.props.name, - this.props.namePrefix, - this.props.selectedPhases, - this.props.selectedLabels, - date, - this.props.maxStartedAt - ); - }} - placeholderText='From' - dateFormat='dd MMM yyyy' - todayButton='Today' - className='argo-field argo-textarea' - /> - { - this.props.onChange( - this.props.namespace, - this.props.name, - this.props.namePrefix, - this.props.selectedPhases, - this.props.selectedLabels, - this.props.minStartedAt, - date - ); - }} - placeholderText='To' - dateFormat='dd MMM yyyy' - todayButton='Today' - className='argo-field argo-textarea' - /> -
-
-
- ); - } - - private getPhaseItems(workflows: models.Workflow[]) { - const phasesMap = new Map(); - this.props.phaseItems.forEach(value => phasesMap.set(value, 0)); - workflows.filter(wf => wf.status.phase).forEach(wf => phasesMap.set(wf.status.phase, (phasesMap.get(wf.status.phase) || 0) + 1)); - const results = new Array<{name: string; count: number}>(); - phasesMap.forEach((val, key) => { - results.push({name: key, count: val}); - }); - return results; - } - - private fetchArchivedWorkflowsLabelKeys(): void { - services.archivedWorkflows.listLabelKeys(this.props.namespace).then(list => { - this.setState({ - labels: list.items?.sort((a, b) => a.localeCompare(b)) || [] - }); - }); - } - - private async fetchArchivedWorkflowsLabels(key: string): Promise { - const labels = await services.archivedWorkflows.listLabelValues(key, this.props?.namespace); - return labels.items.map(i => key + '=' + i).sort((a, b) => a.localeCompare(b)); - } -} diff --git a/ui/src/app/archived-workflows/components/archived-workflow-list/archived-workflow-list.tsx b/ui/src/app/archived-workflows/components/archived-workflow-list/archived-workflow-list.tsx deleted file mode 100644 index a718bae00c77..000000000000 --- a/ui/src/app/archived-workflows/components/archived-workflow-list/archived-workflow-list.tsx +++ /dev/null @@ -1,312 +0,0 @@ -import {Page} from 'argo-ui'; -import * as React from 'react'; -import {Link, RouteComponentProps} from 'react-router-dom'; -import * as models from '../../../../models'; -import {Workflow} from '../../../../models'; -import {uiUrl} from '../../../shared/base'; -import {BasePage} from '../../../shared/components/base-page'; -import {ErrorNotice} from '../../../shared/components/error-notice'; -import {Loading} from '../../../shared/components/loading'; -import {PaginationPanel} from '../../../shared/components/pagination-panel'; -import {PhaseIcon} from '../../../shared/components/phase-icon'; -import {Timestamp} from '../../../shared/components/timestamp'; -import {ZeroState} from '../../../shared/components/zero-state'; -import {formatDuration, wfDuration} from '../../../shared/duration'; -import {Pagination, parseLimit} from '../../../shared/pagination'; -import {ScopedLocalStorage} from '../../../shared/scoped-local-storage'; -import {services} from '../../../shared/services'; -import {Utils} from '../../../shared/utils'; -import {ArchivedWorkflowFilters} from '../archived-workflow-filters/archived-workflow-filters'; - -interface BrowserStorageOptions { - pagination: Pagination; - namespace: string; - name: string; - namePrefix: string; - selectedPhases: string[]; - selectedLabels: string[]; - minStartedAt?: Date; - maxStartedAt?: Date; - error?: Error; - deep: boolean; -} - -interface State extends BrowserStorageOptions { - workflows?: Workflow[]; -} - -const defaultPaginationLimit = 10; - -export class ArchivedWorkflowList extends BasePage, State> { - private storage: ScopedLocalStorage; - - constructor(props: RouteComponentProps, context: any) { - super(props, context); - this.storage = new ScopedLocalStorage('ArchiveListOptions'); - const savedOptions = this.storage.getItem('options', { - pagination: {limit: defaultPaginationLimit}, - selectedPhases: [], - selectedLabels: [] - } as State); - const phaseQueryParam = this.queryParams('phase'); - const labelQueryParam = this.queryParams('label'); - this.state = { - pagination: {offset: this.queryParam('offset'), limit: parseLimit(this.queryParam('limit')) || savedOptions.pagination.limit}, - namespace: Utils.getNamespace(this.props.match.params.namespace) || '', - name: this.queryParams('name').toString() || '', - namePrefix: this.queryParams('namePrefix').toString() || '', - selectedPhases: phaseQueryParam.length > 0 ? phaseQueryParam : savedOptions.selectedPhases, - selectedLabels: labelQueryParam.length > 0 ? labelQueryParam : savedOptions.selectedLabels, - minStartedAt: this.parseTime(this.queryParam('minStartedAt')) || this.lastMonth(), - maxStartedAt: this.parseTime(this.queryParam('maxStartedAt')) || this.nextDay(), - deep: this.queryParam('deep') === 'true' - }; - } - - public componentDidMount(): void { - this.fetchArchivedWorkflows( - this.state.namespace, - this.state.name, - this.state.namePrefix, - this.state.selectedPhases, - this.state.selectedLabels, - this.state.minStartedAt, - this.state.maxStartedAt, - this.state.pagination - ); - services.info.collectEvent('openedArchivedWorkflowList').then(); - } - - public componentDidUpdate(): void { - if (this.state.deep === true && this.state.workflows && this.state.workflows.length === 1) { - const workflow = this.state.workflows[0]; - const url = '/archived-workflows/' + workflow.metadata.namespace + '/' + (workflow.metadata.uid || ''); - this.props.history.push(url); - } - } - - public render() { - return ( - -
-
-
- - this.changeFilters(namespace, name, namePrefix, selectedPhases, selectedLabels, minStartedAt, maxStartedAt, { - limit: this.state.pagination.limit - }) - } - /> -
-
-
{this.renderWorkflows()}
-
-
- ); - } - - private lastMonth() { - const dt = new Date(); - dt.setMonth(dt.getMonth() - 1); - dt.setHours(0, 0, 0, 0); - return dt; - } - - private nextDay() { - const dt = new Date(); - dt.setDate(dt.getDate() + 1); - dt.setHours(0, 0, 0, 0); - return dt; - } - - private parseTime(dateStr: string) { - if (dateStr != null) { - return new Date(dateStr); - } - } - - private changeFilters( - namespace: string, - name: string, - namePrefix: string, - selectedPhases: string[], - selectedLabels: string[], - minStartedAt: Date, - maxStartedAt: Date, - pagination: Pagination - ) { - this.fetchArchivedWorkflows(namespace, name, namePrefix, selectedPhases, selectedLabels, minStartedAt, maxStartedAt, pagination); - } - - private get filterParams() { - const params = new URLSearchParams(); - if (this.state.selectedPhases) { - this.state.selectedPhases.forEach(phase => { - params.append('phase', phase); - }); - } - if (this.state.selectedLabels) { - this.state.selectedLabels.forEach(label => { - params.append('label', label); - }); - } - if (this.state.name) { - params.append('name', this.state.name); - } - if (this.state.namePrefix) { - params.append('namePrefix', this.state.namePrefix); - } - params.append('minStartedAt', this.state.minStartedAt.toISOString()); - params.append('maxStartedAt', this.state.maxStartedAt.toISOString()); - if (this.state.pagination.offset) { - params.append('offset', this.state.pagination.offset); - } - if (this.state.pagination.limit !== null && this.state.pagination.limit !== defaultPaginationLimit) { - params.append('limit', this.state.pagination.limit.toString()); - } - return params; - } - - private fetchBrowserStorageStateObject(state: State): BrowserStorageOptions { - const browserStorageOptions: BrowserStorageOptions = {} as BrowserStorageOptions; - browserStorageOptions.deep = state.deep; - browserStorageOptions.error = state.error; - browserStorageOptions.maxStartedAt = state.maxStartedAt; - browserStorageOptions.minStartedAt = state.minStartedAt; - browserStorageOptions.name = state.name; - browserStorageOptions.namePrefix = state.namePrefix; - browserStorageOptions.namespace = state.namespace; - browserStorageOptions.pagination = state.pagination; - browserStorageOptions.selectedLabels = state.selectedLabels; - browserStorageOptions.selectedPhases = state.selectedPhases; - return browserStorageOptions; - } - - private saveHistory() { - this.storage.setItem('options', this.fetchBrowserStorageStateObject(this.state), {} as BrowserStorageOptions); - const newNamespace = Utils.managedNamespace ? '' : this.state.namespace; - this.url = uiUrl('archived-workflows' + (newNamespace ? '/' + newNamespace : '') + '?' + this.filterParams.toString()); - Utils.currentNamespace = this.state.namespace; - } - - private fetchArchivedWorkflows( - namespace: string, - name: string, - namePrefix: string, - selectedPhases: string[], - selectedLabels: string[], - minStartedAt: Date, - maxStartedAt: Date, - pagination: Pagination - ): void { - services.archivedWorkflows - .list(namespace, name, namePrefix, selectedPhases, selectedLabels, minStartedAt, maxStartedAt, pagination) - .then(list => { - this.setState( - { - error: null, - namespace, - name, - namePrefix, - workflows: list.items || [], - selectedPhases, - selectedLabels, - minStartedAt, - maxStartedAt, - pagination: { - limit: pagination.limit, - offset: pagination.offset, - nextOffset: list.metadata.continue - } - }, - this.saveHistory - ); - }) - .catch(error => this.setState({error})); - } - - private renderWorkflows() { - if (this.state.error) { - return ; - } - if (!this.state.workflows) { - return ; - } - const learnMore = Learn more; - if (this.state.workflows.length === 0) { - return ( - -

To add entries to the archive you must enable archiving in configuration. Records are created in the archive on workflow completion.

-

{learnMore}.

-
- ); - } - - return ( - <> -
-
-
-
NAME
-
NAMESPACE
-
STARTED
-
FINISHED
-
DURATION
-
- {this.state.workflows.map(w => ( - -
- -
-
{w.metadata.name}
-
{w.metadata.namespace}
-
- -
-
- -
-
{formatDuration(wfDuration(w.status))}
- - ))} -
- - this.changeFilters( - this.state.namespace, - this.state.name, - this.state.namePrefix, - this.state.selectedPhases, - this.state.selectedLabels, - this.state.minStartedAt, - this.state.maxStartedAt, - pagination - ) - } - pagination={this.state.pagination} - numRecords={(this.state.workflows || []).length} - /> -

- Records are created in the archive when a workflow completes. {learnMore}. -

- - ); - } -} diff --git a/ui/src/app/archived-workflows/index.ts b/ui/src/app/archived-workflows/index.ts deleted file mode 100644 index 36b663139f39..000000000000 --- a/ui/src/app/archived-workflows/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {ArchivedWorkflowContainer} from './components/archived-workflow-container'; - -export default { - component: ArchivedWorkflowContainer -}; diff --git a/ui/src/app/reports/components/reports.tsx b/ui/src/app/reports/components/reports.tsx index e1eee65df22e..a4feac527964 100644 --- a/ui/src/app/reports/components/reports.tsx +++ b/ui/src/app/reports/components/reports.tsx @@ -1,4 +1,4 @@ -import {Checkbox, Page} from 'argo-ui/src/index'; +import {Page} from 'argo-ui/src/index'; import {ChartOptions} from 'chart.js'; import 'chartjs-plugin-annotation'; import * as React from 'react'; @@ -25,7 +25,6 @@ interface Chart { } interface State { - archivedWorkflows: boolean; namespace: string; labels: string[]; autocompleteLabels: string[]; @@ -58,7 +57,6 @@ export class Reports extends BasePage, State> { constructor(props: RouteComponentProps, context: any) { super(props, context); this.state = { - archivedWorkflows: !!this.queryParam('archivedWorkflows'), namespace: Utils.getNamespace(this.props.match.params.namespace) || '', labels: (this.queryParam('labels') || '').split(',').filter(v => v !== ''), autocompleteLabels: [''] @@ -66,8 +64,7 @@ export class Reports extends BasePage, State> { } public componentDidMount() { - this.fetchReport(this.state.namespace, this.state.labels, this.state.archivedWorkflows); - this.fetchWorkflowsLabels(this.state.archivedWorkflows); + this.fetchReport(this.state.namespace, this.state.labels); services.info.collectEvent('openedReports').then(); } @@ -99,38 +96,30 @@ export class Reports extends BasePage, State> { } private setLabel(name: string, value: string) { - this.fetchReport(this.state.namespace, this.state.labels.filter(label => !label.startsWith(name)).concat(name + '=' + value), this.state.archivedWorkflows); + this.fetchReport(this.state.namespace, this.state.labels.filter(label => !label.startsWith(name)).concat(name + '=' + value)); } - private fetchReport(namespace: string, labels: string[], archivedWorkflows: boolean) { + private fetchReport(namespace: string, labels: string[]) { if (namespace === '' || labels.length === 0) { - this.setState({namespace, labels, archivedWorkflows, charts: null}); + this.setState({namespace, labels, charts: null}); return; } - (archivedWorkflows - ? services.archivedWorkflows.list(namespace, '', '', [], labels, null, null, {limit}) - : services.workflows.list(namespace, [], labels, {limit}, [ - 'items.metadata.name', - 'items.status.phase', - 'items.status.startedAt', - 'items.status.finishedAt', - 'items.status.resourcesDuration' - ]) - ) + services.workflows + .list(namespace, [], labels, {limit}, [ + 'items.metadata.name', + 'items.status.phase', + 'items.status.startedAt', + 'items.status.finishedAt', + 'items.status.resourcesDuration' + ]) .then(list => this.getExtractDatasets(list.items || [])) - .then(charts => this.setState({error: null, charts, namespace, labels, archivedWorkflows}, this.saveHistory)) + .then(charts => this.setState({error: null, charts, namespace, labels}, this.saveHistory)) .catch(error => this.setState({error})); } private saveHistory() { const newNamespace = Utils.managedNamespace ? '' : this.state.namespace; - this.url = uiUrl( - 'reports' + - (newNamespace ? '/' + newNamespace : '') + - '?labels=' + - this.state.labels.join(',') + - (this.state.archivedWorkflows ? '&archivedWorkflows=' + this.state.archivedWorkflows : '') - ); + this.url = uiUrl('reports' + (newNamespace ? '/' + newNamespace : '') + '?labels=' + this.state.labels.join(',')); Utils.currentNamespace = this.state.namespace; } @@ -257,42 +246,16 @@ export class Reports extends BasePage, State> { ]; } - private fetchWorkflowsLabels(isArchivedWorkflows: boolean): void { - if (isArchivedWorkflows) { - services.archivedWorkflows.listLabelKeys(this.state.namespace).then(list => { - this.setState({ - autocompleteLabels: list.items?.sort((a, b) => a.localeCompare(b)) || [] - }); - }); - } - } - - private fetchArchivedWorkflowsLabels(key: string): Promise { - return services.archivedWorkflows.listLabelValues(key, this.state.namespace).then(list => { - return list.items.map(i => key + '=' + i).sort((a, b) => a.localeCompare(b)); - }); - } - private renderFilters() { return (
-
-

Archived Workflows

- { - this.fetchReport(this.state.namespace, this.state.labels, checked); - this.fetchWorkflowsLabels(checked); - }} - /> -

Namespace

{ - this.fetchReport(namespace, this.state.labels, this.state.archivedWorkflows); + this.fetchReport(namespace, this.state.labels); }} />
@@ -301,9 +264,8 @@ export class Reports extends BasePage, State> { this.fetchReport(this.state.namespace, labels, this.state.archivedWorkflows)} + autocomplete={this.state.autocompleteLabels} + onChange={labels => this.fetchReport(this.state.namespace, labels)} />
diff --git a/ui/src/app/shared/services/archived-workflows-service.ts b/ui/src/app/shared/services/archived-workflows-service.ts deleted file mode 100644 index ecda26a10c27..000000000000 --- a/ui/src/app/shared/services/archived-workflows-service.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as models from '../../../models'; -import {Pagination} from '../pagination'; -import {Utils} from '../utils'; -import requests from './requests'; -export const ArchivedWorkflowsService = { - list(namespace: string, name: string, namePrefix: string, phases: string[], labels: string[], minStartedAt: Date, maxStartedAt: Date, pagination: Pagination) { - if (namespace === '') { - return requests - .get(`api/v1/archived-workflows?${Utils.queryParams({name, namePrefix, phases, labels, minStartedAt, maxStartedAt, pagination}).join('&')}`) - .then(res => res.body as models.WorkflowList); - } else { - return requests - .get(`api/v1/archived-workflows?namespace=${namespace}&${Utils.queryParams({name, namePrefix, phases, labels, minStartedAt, maxStartedAt, pagination}).join('&')}`) - .then(res => res.body as models.WorkflowList); - } - }, - - get(uid: string, namespace: string) { - if (namespace === '') { - return requests.get(`api/v1/archived-workflows/${uid}`).then(res => res.body as models.Workflow); - } else { - return requests.get(`api/v1/archived-workflows/${uid}?namespace=${namespace}`).then(res => res.body as models.Workflow); - } - }, - - delete(uid: string, namespace: string) { - if (namespace === '') { - return requests.delete(`api/v1/archived-workflows/${uid}`); - } else { - return requests.delete(`api/v1/archived-workflows/${uid}?namespace=${namespace}`); - } - }, - - listLabelKeys(namespace: string) { - if (namespace === '') { - return requests.get(`api/v1/archived-workflows-label-keys`).then(res => res.body as models.Labels); - } else { - return requests.get(`api/v1/archived-workflows-label-keys?namespace=${namespace}`).then(res => res.body as models.Labels); - } - }, - - async listLabelValues(key: string, namespace: string): Promise { - let url = `api/v1/archived-workflows-label-values?listOptions.labelSelector=${key}`; - if (namespace !== '') { - url += `&namespace=${namespace}`; - } - return (await requests.get(url)).body as models.Labels; - }, - - resubmit(uid: string, namespace: string) { - return requests - .put(`api/v1/archived-workflows/${uid}/resubmit`) - .send({namespace}) - .then(res => res.body as models.Workflow); - }, - - retry(uid: string, namespace: string) { - return requests - .put(`api/v1/archived-workflows/${uid}/retry`) - .send({namespace}) - .then(res => res.body as models.Workflow); - } -}; diff --git a/ui/src/app/shared/services/index.ts b/ui/src/app/shared/services/index.ts index d44e7364be1c..29e9b1326ea5 100644 --- a/ui/src/app/shared/services/index.ts +++ b/ui/src/app/shared/services/index.ts @@ -1,4 +1,3 @@ -import {ArchivedWorkflowsService} from './archived-workflows-service'; import {ClusterWorkflowTemplateService} from './cluster-workflow-template-service'; import {CronWorkflowService} from './cron-workflow-service'; import {EventService} from './event-service'; @@ -16,7 +15,6 @@ interface Services { workflows: typeof WorkflowsService; workflowTemplate: typeof WorkflowTemplateService; clusterWorkflowTemplate: typeof ClusterWorkflowTemplateService; - archivedWorkflows: typeof ArchivedWorkflowsService; cronWorkflows: typeof CronWorkflowService; } @@ -28,6 +26,5 @@ export const services: Services = { event: EventService, eventSource: EventSourceService, sensor: SensorService, - archivedWorkflows: ArchivedWorkflowsService, cronWorkflows: CronWorkflowService }; diff --git a/ui/src/app/shared/services/workflows-service.ts b/ui/src/app/shared/services/workflows-service.ts index 9900a10920fa..df70d6d60171 100644 --- a/ui/src/app/shared/services/workflows-service.ts +++ b/ui/src/app/shared/services/workflows-service.ts @@ -66,6 +66,10 @@ export const WorkflowsService = { return requests.get(`api/v1/workflows/${namespace}/${name}`).then(res => res.body as Workflow); }, + getArchived(namespace: string, name: string) { + return requests.get(`api/v1/archived-workflows/?name=${name}&namespace=${namespace}`).then(res => res.body as models.Workflow); + }, + watch(query: { namespace?: string; name?: string; @@ -151,6 +155,14 @@ export const WorkflowsService = { return requests.delete(`api/v1/workflows/${namespace}/${name}`).then(res => res.body as WorkflowDeleteResponse); }, + deleteArchived(uid: string, namespace: string): Promise { + if (namespace === '') { + return requests.delete(`api/v1/archived-workflows/${uid}`).then(res => res.body as WorkflowDeleteResponse); + } else { + return requests.delete(`api/v1/archived-workflows/${uid}?namespace=${namespace}`).then(res => res.body as WorkflowDeleteResponse); + } + }, + submit(kind: string, name: string, namespace: string, submitOptions?: SubmitOpts) { return requests .post(`api/v1/workflows/${namespace}/submit`) diff --git a/ui/src/app/shared/workflow-operations-map.ts b/ui/src/app/shared/workflow-operations-map.tsx similarity index 100% rename from ui/src/app/shared/workflow-operations-map.ts rename to ui/src/app/shared/workflow-operations-map.tsx diff --git a/ui/src/app/workflows/components/workflow-details/workflow-details.tsx b/ui/src/app/workflows/components/workflow-details/workflow-details.tsx index f61bdce5011d..e13d1e771df0 100644 --- a/ui/src/app/workflows/components/workflow-details/workflow-details.tsx +++ b/ui/src/app/workflows/components/workflow-details/workflow-details.tsx @@ -3,7 +3,7 @@ import * as classNames from 'classnames'; import * as React from 'react'; import {useContext, useEffect, useRef, useState} from 'react'; import {RouteComponentProps} from 'react-router'; -import {ArtifactRepository, execSpec, Link, NodeStatus, Parameter, Workflow} from '../../../../models'; +import {ArtifactRepository, execSpec, isArchivedWorkflow, Link, NodeStatus, Parameter, Workflow} from '../../../../models'; import {ANNOTATION_KEY_POD_NAME_VERSION} from '../../../shared/annotations'; import {artifactRepoHasLocation, findArtifact} from '../../../shared/artifacts'; import {uiUrl} from '../../../shared/base'; @@ -51,12 +51,49 @@ const INITIAL_SIDE_PANEL_WIDTH = 570; const ANIMATION_MS = 200; const ANIMATION_BUFFER_MS = 20; +// This is used instead of React state since the state update is async and there's a delay for parent +// component to render with the updated state. +let globalDeleteArchived = false; + +const DeleteCheck = (props: {isWfInDB: boolean; isWfInCluster: boolean}) => { + // The local states are created intentionally so that the checkbox works as expected + const [da, sda] = useState(false); + if (props.isWfInDB && props.isWfInCluster) { + return ( + <> +

Are you sure you want to delete this workflow?

+
+ { + sda(!da); + globalDeleteArchived = !globalDeleteArchived; + }} + id='delete-check' + /> + +
+ + ); + } else { + return ( + <> +

Are you sure you want to delete this workflow?

+ + ); + } +}; + export const WorkflowDetails = ({history, location, match}: RouteComponentProps) => { // boiler-plate const {navigation, popup} = useContext(Context); const queryParams = new URLSearchParams(location.search); const [namespace] = useState(match.params.namespace); + const [isWfInDB, setIsWfInDB] = useState(false); + const [isWfInCluster, setIsWfInCluster] = useState(false); const [name, setName] = useState(match.params.name); const [tab, setTab] = useState(queryParams.get('tab') || 'workflow'); const [nodeId, setNodeId] = useState(queryParams.get('nodeId')); @@ -147,20 +184,45 @@ export const WorkflowDetails = ({history, location, match}: RouteComponentProps< title: workflowOperation.title.charAt(0).toUpperCase() + workflowOperation.title.slice(1), iconClassName: workflowOperation.iconClassName, action: () => { - popup.confirm('Confirm', `Are you sure you want to ${workflowOperation.title.toLowerCase()} this workflow?`).then(yes => { - if (yes) { - workflowOperation - .action(workflow) - .then((wf: Workflow) => { - if (workflowOperation.title === 'DELETE') { - navigation.goto(uiUrl(`workflows/${workflow.metadata.namespace}`)); - } else { - setName(wf.metadata.name); + if (workflowOperation.title === 'DELETE') { + popup + .confirm('Confirm', () => ) + .then(yes => { + if (yes) { + if (isWfInCluster) { + services.workflows + .delete(workflow.metadata.name, workflow.metadata.namespace) + .then(() => { + setIsWfInCluster(false); + }) + .catch(setError); + } + if (isWfInDB && (globalDeleteArchived || !isWfInCluster)) { + services.workflows + .deleteArchived(workflow.metadata.uid, workflow.metadata.namespace) + .then(() => { + setIsWfInDB(false); + }) + .catch(setError); } - }) - .catch(setError); - } - }); + navigation.goto(uiUrl(`workflows/${workflow.metadata.namespace}`)); + // TODO: This is a temporary workaround so that the list of workflows + // is correctly displayed. Workflow list page needs to be more responsive. + window.location.reload(); + } + }); + } else { + popup.confirm('Confirm', `Are you sure you want to ${workflowOperation.title.toLowerCase()} this workflow?`).then(yes => { + if (yes) { + workflowOperation + .action(workflow) + .then((wf: Workflow) => { + setName(wf.metadata.name); + }) + .catch(setError); + } + }); + } } }; }); @@ -277,7 +339,7 @@ export const WorkflowDetails = ({history, location, match}: RouteComponentProps< )}
Artifacts
- +
@@ -286,38 +348,51 @@ export const WorkflowDetails = ({history, location, match}: RouteComponentProps< ); }; - useEffect(() => { const retryWatch = new RetryWatch( () => services.workflows.watch({name, namespace}), - () => setError(null), + () => { + setIsWfInCluster(true); + setError(null); + }, e => { if (e.type === 'DELETED') { setError(new Error('Workflow gone')); + setIsWfInCluster(false); } else { if (hasArtifactGCError(e.object.status.conditions)) { setError(new Error('Artifact garbage collection failed')); } setWorkflow(e.object); + setIsWfInCluster(true); } }, err => { - services.workflows - .get(namespace, name) - .then() - .catch(e => { - if (e.status === 404) { - navigation.goto(historyUrl('archived-workflows', {namespace, name, deep: true})); - } - }); - setError(err); + setIsWfInCluster(false); } ); retryWatch.start(); return () => retryWatch.stop(); }, [namespace, name]); + useEffect(() => { + if (!workflow && !isWfInCluster) { + services.workflows + .getArchived(namespace, name) + .then(wf => { + setError(null); + setWorkflow(wf); + setIsWfInDB(true); + }) + .catch(newErr => { + if (newErr.status !== 404) { + setError(newErr); + } + }); + } + }, [namespace, name, isWfInCluster]); + const openLink = (link: Link) => { const object = { metadata: { @@ -462,7 +537,7 @@ export const WorkflowDetails = ({history, location, match}: RouteComponentProps< onShowContainerLogs={(x, container) => setSidePanel(`logs:${x}:${container}`)} onShowEvents={() => setSidePanel(`events:${nodeId}`)} onShowYaml={() => setSidePanel(`yaml:${nodeId}`)} - archived={false} + archived={isArchivedWorkflow(workflow)} onResume={() => renderResumePopup()} /> )} @@ -474,7 +549,13 @@ export const WorkflowDetails = ({history, location, match}: RouteComponentProps< {workflow && ( setSidePanel(null)}> {parsedSidePanel.type === 'logs' && ( - + )} {parsedSidePanel.type === 'events' && } {parsedSidePanel.type === 'share' && } diff --git a/ui/src/app/workflows/components/workflows-list/workflows-list.tsx b/ui/src/app/workflows/components/workflows-list/workflows-list.tsx index 9c5a9589e5ec..3651a4f5c1bf 100644 --- a/ui/src/app/workflows/components/workflows-list/workflows-list.tsx +++ b/ui/src/app/workflows/components/workflows-list/workflows-list.tsx @@ -380,6 +380,7 @@ export class WorkflowsList extends BasePage, State> {
PROGRESS
MESSAGE
DETAILS
+
ARCHIVED
{(this.state.columns || []).map(col => { return (
diff --git a/ui/src/app/workflows/components/workflows-row/workflows-row.tsx b/ui/src/app/workflows/components/workflows-row/workflows-row.tsx index 6e24d1f1fbb4..7e6b44b0ae07 100644 --- a/ui/src/app/workflows/components/workflows-row/workflows-row.tsx +++ b/ui/src/app/workflows/components/workflows-row/workflows-row.tsx @@ -2,7 +2,7 @@ import {Ticker} from 'argo-ui/src/index'; import * as React from 'react'; import {Link} from 'react-router-dom'; import * as models from '../../../../models'; -import {Workflow} from '../../../../models'; +import {isArchivedWorkflow, Workflow} from '../../../../models'; import {ANNOTATION_DESCRIPTION, ANNOTATION_TITLE} from '../../../shared/annotations'; import {uiUrl} from '../../../shared/base'; import {DurationPanel} from '../../../shared/components/duration-panel'; @@ -87,6 +87,7 @@ export class WorkflowsRow extends React.Component
+
{isArchivedWorkflow(wf) ? 'true' : 'false'}
{(this.props.columns || []).map(column => { const value = wf.metadata.labels[column.key]; return ( diff --git a/ui/src/models/workflows.ts b/ui/src/models/workflows.ts index b5286a62c5b8..e8a653e492db 100644 --- a/ui/src/models/workflows.ts +++ b/ui/src/models/workflows.ts @@ -539,6 +539,15 @@ export interface Workflow { export const execSpec = (w: Workflow) => Object.assign({}, w.status.storedWorkflowTemplateSpec, w.spec); +// The label may not have been updated on time but usually this indicates that they are already archived. +export function isArchivedWorkflow(wf: Workflow): boolean { + return ( + wf.metadata.labels && + (wf.metadata.labels['workflows.argoproj.io/workflow-archiving-status'] === 'Archived' || + wf.metadata.labels['workflows.argoproj.io/workflow-archiving-status'] === 'Pending') + ); +} + export type NodeType = 'Pod' | 'Container' | 'Steps' | 'StepGroup' | 'DAG' | 'Retry' | 'Skipped' | 'TaskGroup' | 'Suspend'; export interface NodeStatus {