Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): Add pagination to workflow list. Fixes #1080 and #976 #2863

Merged
merged 6 commits into from
Apr 28, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
changes
  • Loading branch information
alexec committed Apr 28, 2020
commit 4a0d9c84e40c078d2459e0f5407aa48ae942d737
2 changes: 1 addition & 1 deletion server/workflow/workflow_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ func (s *workflowServer) ListWorkflows(ctx context.Context, req *workflowpkg.Wor
}
}

return &v1alpha1.WorkflowList{Items: wfList.Items}, nil
return &v1alpha1.WorkflowList{ListMeta: metav1.ListMeta{Continue: wfList.Continue}, Items: wfList.Items}, nil
}

func (s *workflowServer) WatchWorkflows(req *workflowpkg.WatchWorkflowsRequest, ws workflowpkg.WorkflowService_WatchWorkflowsServer) error {
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/argo_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,8 @@ func (s *ArgoServerSuite) TestWorkflowService() {
Expect().
Status(200).
JSON()
j.Path("$.metadata").
NotNull()
j.
Path("$.items").
Array().
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {Page, SlidingPanel} from 'argo-ui';

import {Ticker} from 'argo-ui/src/index';
import * as classNames from 'classnames';
import * as moment from 'moment';
import * as React from 'react';
import {Link, RouteComponentProps} from 'react-router-dom';
import * as models from '../../../../models';
Expand All @@ -15,9 +13,9 @@ import {ResourceSubmit} from '../../../shared/components/resource-submit';
import {Timestamp} from '../../../shared/components/timestamp';
import {ZeroState} from '../../../shared/components/zero-state';
import {Consumer} from '../../../shared/context';
import {formatDuration} from '../../../shared/duration';
import {formatDuration, wfDuration} from '../../../shared/duration';
import {exampleWorkflow} from '../../../shared/examples';
import {Pagination, parseLimit} from '../../../shared/pagination';
import {defaultPaginationLimit, Pagination, parseLimit} from '../../../shared/pagination';
import {services} from '../../../shared/services';
import {Utils} from '../../../shared/utils';
import {ArchivedWorkflowFilters} from '../archived-workflow-filters/archived-workflow-filters';
Expand Down Expand Up @@ -45,7 +43,7 @@ export class ArchivedWorkflowList extends BasePage<RouteComponentProps<any>, Sta
super(props, context);
this.state = {
loading: true,
pagination: {offset: this.queryParam('offset') || '', limit: parseLimit(this.queryParam('limit'))},
pagination: {offset: this.queryParam('offset'), limit: parseLimit(this.queryParam('limit'))},
initialized: false,
managedNamespace: false,
namespace: this.props.match.params.namespace || Utils.getCurrentNamespace() || '',
Expand Down Expand Up @@ -161,7 +159,9 @@ export class ArchivedWorkflowList extends BasePage<RouteComponentProps<any>, Sta
if (pagination.offset) {
params.append('offset', pagination.offset);
}
params.append('limit', pagination.limit.toString());
if (pagination.limit !== defaultPaginationLimit) {
params.append('limit', pagination.limit.toString());
}
const url = 'archived-workflows/' + namespace + '?' + params.toString();
history.pushState(null, '', uiUrl(url));
this.fetchArchivedWorkflows(namespace, selectedPhases, selectedLabels, minStartedAt, maxStartedAt, pagination);
Expand Down Expand Up @@ -194,8 +194,8 @@ export class ArchivedWorkflowList extends BasePage<RouteComponentProps<any>, Sta
minStartedAt,
maxStartedAt,
pagination: {
limit: this.state.pagination.limit,
offset: this.state.pagination.offset,
limit: pagination.limit,
offset: pagination.offset,
nextOffset: list.metadata.continue
},
loading: false
Expand All @@ -219,11 +219,6 @@ export class ArchivedWorkflowList extends BasePage<RouteComponentProps<any>, Sta
);
}

function wfDuration(workflow: models.WorkflowStatus, now: moment.Moment) {
const endTime = workflow.finishedAt ? moment(workflow.finishedAt) : now;
return endTime.diff(moment(workflow.startedAt)) / 1000;
}

return (
<>
<div className='argo-table-list'>
Expand All @@ -243,25 +238,16 @@ export class ArchivedWorkflowList extends BasePage<RouteComponentProps<any>, Sta
<div className='columns small-3'>{w.metadata.namespace}</div>
<div className='columns small-2'>
<Timestamp date={w.status.startedAt} />
<Timestamp date={w.status.finishedAt} />
</div>
<div className='columns small-2'>
<Ticker>{now => formatDuration(wfDuration(w.status, now))}</Ticker>
</div>
<div className='columns small-2'>{formatDuration(wfDuration(w.status))}</div>
</Link>
))}
</div>
<PaginationPanel
onChange={pagination => {
this.setState({pagination});
this.changeFilters(
this.state.namespace,
this.state.selectedPhases,
this.state.selectedLabels,
this.state.minStartedAt,
this.state.maxStartedAt,
pagination
);
}}
onChange={pagination =>
this.changeFilters(this.state.namespace, this.state.selectedPhases, this.state.selectedLabels, this.state.minStartedAt, this.state.maxStartedAt, pagination)
}
pagination={this.state.pagination}
/>
<p>
Expand Down
4 changes: 2 additions & 2 deletions ui/src/app/shared/components/pagination-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from 'react';
import {Pagination} from '../pagination';
import {Pagination, parseLimit} from '../pagination';

export class PaginationPanel extends React.Component<{pagination: Pagination; onChange: (pagination: Pagination) => void}> {
public render() {
Expand Down Expand Up @@ -28,7 +28,7 @@ export class PaginationPanel extends React.Component<{pagination: Pagination; on
onChange={e =>
this.props.onChange({
offset: this.props.pagination.offset,
limit: parseInt(e.target.value)
limit: parseLimit(e.target.value)
})
}
value={this.props.pagination.limit}>
Expand Down
6 changes: 6 additions & 0 deletions ui/src/app/shared/duration.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as models from '../../models';

/**
* Format the given number number of seconds in the form _d_h_m_s.
* @param seconds Number of seconds to format. Will be rounded to the nearest whole number.
Expand All @@ -24,3 +26,7 @@ export function formatDuration(seconds: number) {

return formattedDuration;
}

export function wfDuration(status: models.WorkflowStatus) {
return ((status.finishedAt ? new Date(status.finishedAt) : new Date()).getTime() - new Date(status.startedAt).getTime()) / 1000;
}
6 changes: 4 additions & 2 deletions ui/src/app/shared/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ export interface Pagination {
nextOffset?: string;
}

export const defaultPaginationLimit = 10;

export function parseLimit(str: string) {
const v = parseInt(str);
return isNaN(v) ? 10 : v;
const v = parseInt(str, 10);
return isNaN(v) ? defaultPaginationLimit : v;
}
2 changes: 1 addition & 1 deletion ui/src/app/shared/services/archived-workflows-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import requests from './requests';
export class ArchivedWorkflowsService {
public list(namespace: string, phases: string[], labels: string[], minStartedAt: Date, maxStartedAt: Date, pagination: Pagination) {
return requests
.get(`api/v1/archived-workflows?limit=${this.queryParams({namespace, phases, labels, minStartedAt, maxStartedAt, pagination}).join('&')}`)
.get(`api/v1/archived-workflows?${this.queryParams({namespace, phases, labels, minStartedAt, maxStartedAt, pagination}).join('&')}`)
.then(res => res.body as models.WorkflowList);
}

Expand Down
7 changes: 3 additions & 4 deletions ui/src/app/shared/services/workflows-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import {Pagination} from '../pagination';
import requests from './requests';
import {WorkflowDeleteResponse} from './responses';

const fieldsFilter = `fields=items.metadata.name,items.metadata.namespace,items.status.phase,items.status.finishedAt,items.status.startedAt`;

export class WorkflowsService {
public create(workflow: Workflow, namespace: string) {
return requests
Expand All @@ -23,15 +21,16 @@ export class WorkflowsService {
params.push(`listOptions.continue=${pagination.offset}`);
}
params.push(`listOptions.limit=${pagination.limit}`);
return requests.get(`api/v1/workflows/${namespace}?${fieldsFilter}&${params.join('&')}`).then(res => res.body as WorkflowList);
params.push(`fields=metadata,items.metadata.name,items.metadata.namespace,items.status.phase,items.status.finishedAt,items.status.startedAt`);
return requests.get(`api/v1/workflows/${namespace}?${params.join('&')}`).then(res => res.body as WorkflowList);
}

public get(namespace: string, name: string) {
return requests.get(`api/v1/workflows/${namespace}/${name}`).then(res => res.body as Workflow);
}

public watch(filter: {namespace?: string; name?: string; phases?: Array<string>; labels?: Array<string>}): Observable<models.kubernetes.WatchEvent<Workflow>> {
const url = `api/v1/workflow-events/${filter.namespace || ''}?${fieldsFilter}&${this.queryParams(filter).join('&')}`;
const url = `api/v1/workflow-events/${filter.namespace || ''}?${this.queryParams(filter).join('&')}`;

return requests.loadEventSource(url, true).map(data => JSON.parse(data).result as models.kubernetes.WatchEvent<Workflow>);
}
Expand Down
13 changes: 5 additions & 8 deletions ui/src/app/workflows/components/workflow-summary-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
import {Duration, Ticker} from 'argo-ui';
import * as moment from 'moment';
import {Ticker} from 'argo-ui';
import * as React from 'react';

import {NODE_PHASE, Workflow} from '../../../models';
import {ConditionsPanel} from '../../shared/conditions-panel';
import {formatDuration, wfDuration} from '../../shared/duration';
import {ResourcesDuration} from '../../shared/resources-duration';

export const WorkflowSummaryPanel = (props: {workflow: Workflow}) => (
<Ticker disabled={props.workflow && props.workflow.status.phase !== NODE_PHASE.RUNNING}>
{now => {
const endTime = props.workflow.status.finishedAt ? moment(props.workflow.status.finishedAt) : now;
const duration = endTime.diff(moment(props.workflow.status.startedAt)) / 1000;

const attributes = [
{() => {
const attributes: {title: string; value: any}[] = [
{title: 'Status', value: props.workflow.status.phase},
{title: 'Message', value: props.workflow.status.message},
{title: 'Name', value: props.workflow.metadata.name},
{title: 'Namespace', value: props.workflow.metadata.namespace},
{title: 'Started At', value: props.workflow.status.startedAt},
{title: 'Finished At', value: props.workflow.status.finishedAt || '-'},
{title: 'Duration', value: <Duration durationMs={duration} />}
{title: 'Duration', value: formatDuration(wfDuration(props.workflow.status))}
];
if (props.workflow.status.resourcesDuration) {
attributes.push({
Expand Down
50 changes: 25 additions & 25 deletions ui/src/app/workflows/components/workflows-list/workflows-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {Subscription} from 'rxjs';

import {Autocomplete, Page, SlidingPanel} from 'argo-ui';
import * as models from '../../../../models';
import {compareWorkflows, Workflow} from '../../../../models';
import {Workflow} from '../../../../models';
import {uiUrl} from '../../../shared/base';
import {Consumer} from '../../../shared/context';
import {services} from '../../../shared/services';
Expand All @@ -19,11 +19,10 @@ import {Utils} from '../../../shared/utils';

import {Ticker} from 'argo-ui/src/index';
import * as classNames from 'classnames';
import * as moment from 'moment';
import {PaginationPanel} from '../../../shared/components/pagination-panel';
import {Timestamp} from '../../../shared/components/timestamp';
import {formatDuration} from '../../../shared/duration';
import {Pagination, parseLimit} from '../../../shared/pagination';
import {formatDuration, wfDuration} from '../../../shared/duration';
import {defaultPaginationLimit, Pagination, parseLimit} from '../../../shared/pagination';
import {WorkflowFilters} from '../workflow-filters/workflow-filters';

require('./workflows-list.scss');
Expand All @@ -44,13 +43,14 @@ export class WorkflowsList extends BasePage<RouteComponentProps<any>, State> {
private get wfInput() {
return Utils.tryJsonParse(this.queryParam('new'));
}

private subscription: Subscription;

constructor(props: RouteComponentProps<State>, context: any) {
super(props, context);
this.state = {
loading: true,
pagination: {offset: this.queryParam('continue'), limit: parseLimit(this.queryParam('limit'))},
pagination: {offset: this.queryParam('offset'), limit: parseLimit(this.queryParam('limit'))},
initialized: false,
managedNamespace: false,
namespace: this.props.match.params.namespace || Utils.getCurrentNamespace() || '',
Expand Down Expand Up @@ -159,10 +159,14 @@ export class WorkflowsList extends BasePage<RouteComponentProps<any>, State> {
workflowList = services.workflows.list(newNamespace, selectedPhases, selectedLabels, pagination);
}
workflowList
.then(list => list.items)
.then(list => list || [])
.then(workflows => {
this.setState({workflows, namespace: newNamespace, selectedPhases, selectedLabels});
.then(wfList => {
this.setState({
workflows: wfList.items || [],
pagination: {offset: pagination.offset, limit: pagination.limit, nextOffset: wfList.metadata.continue},
namespace: newNamespace,
selectedPhases,
selectedLabels
});
Utils.setCurrentNamespace(newNamespace);
})
.then(() => {
Expand All @@ -184,8 +188,6 @@ export class WorkflowsList extends BasePage<RouteComponentProps<any>, State> {
} else {
if (index > -1) {
workflows[index] = workflowChange.object;
} else {
workflows.unshift(workflowChange.object);
}
}
return {workflows, updated: true};
Expand All @@ -209,10 +211,13 @@ export class WorkflowsList extends BasePage<RouteComponentProps<any>, State> {
selectedLabels.forEach(label => {
params.append('label', label);
});
let url = 'workflows/' + namespace;
if (selectedPhases.length > 0 || selectedLabels.length > 0) {
url += '?' + params.toString();
if (pagination.offset) {
params.append('offset', pagination.offset);
}
if (pagination.limit !== defaultPaginationLimit) {
params.append('limit', pagination.limit.toString());
}
const url = 'workflows/' + namespace + '?' + params.toString();
history.pushState(null, '', uiUrl(url));
this.fetchWorkflows(namespace, selectedPhases, selectedLabels, pagination);
}
Expand All @@ -229,11 +234,6 @@ export class WorkflowsList extends BasePage<RouteComponentProps<any>, State> {
</ZeroState>
);
}
function wfDuration(workflow: models.WorkflowStatus, now: moment.Moment) {
const endTime = workflow.finishedAt ? moment(workflow.finishedAt) : now;
return endTime.diff(moment(workflow.startedAt)) / 1000;
}
this.state.workflows.sort(compareWorkflows);

return (
<>
Expand All @@ -246,7 +246,10 @@ export class WorkflowsList extends BasePage<RouteComponentProps<any>, State> {
<div className='columns small-2'>DURATION</div>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: Columns should preferably sum to 12

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1+4+3+2+2=12?

</div>
{this.state.workflows.map(w => (
<Link className='row argo-table-list__row' key={`${w.metadata.uid}`} to={uiUrl(`workflows/${w.metadata.namespace}/${w.metadata.uid}`)}>
<Link
className='row argo-table-list__row'
key={`${w.metadata.namespace}-${w.metadata.name}`}
to={uiUrl(`workflows/${w.metadata.namespace}/${w.metadata.name}`)}>
<div className='columns small-1'>
<i className={classNames('fa', Utils.statusIconClasses(w.status.phase))} />
</div>
Expand All @@ -256,16 +259,13 @@ export class WorkflowsList extends BasePage<RouteComponentProps<any>, State> {
<Timestamp date={w.status.startedAt} />
</div>
<div className='columns small-2'>
<Ticker>{now => formatDuration(wfDuration(w.status, now))}</Ticker>
<Ticker>{() => formatDuration(wfDuration(w.status))}</Ticker>
</div>
</Link>
))}
</div>
<PaginationPanel
onChange={pagination => {
this.setState({pagination});
this.changeFilters(this.state.namespace, this.state.selectedPhases, this.state.selectedLabels, pagination);
}}
onChange={pagination => this.changeFilters(this.state.namespace, this.state.selectedPhases, this.state.selectedLabels, pagination)}
pagination={this.state.pagination}
/>
</>
Expand Down
20 changes: 0 additions & 20 deletions ui/src/models/workflows.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as kubernetes from 'argo-ui/src/models/kubernetes';
import * as moment from 'moment';

/**
* Arguments to a template
Expand Down Expand Up @@ -590,25 +589,6 @@ export interface Workflow {
status?: WorkflowStatus;
}

export function compareWorkflows(first: Workflow, second: Workflow) {
const iStart = first.metadata.creationTimestamp;
const iFinish = (first.status || {finishedAt: null}).finishedAt;
const jStart = second.metadata.creationTimestamp;
const jFinish = (second.status || {finishedAt: null}).finishedAt;

if (!iFinish && !jFinish) {
return moment(jStart).diff(iStart);
}

if (!iFinish && jFinish) {
return -1;
}
if (iFinish && !jFinish) {
return 1;
}
return moment(jStart).diff(iStart);
}

export type NodeType = 'Pod' | 'Steps' | 'StepGroup' | 'DAG' | 'Retry' | 'Skipped' | 'TaskGroup' | 'Suspend';

export interface NodeStatus {
Expand Down