Skip to content

Commit

Permalink
feat(ui): Add pagination to workflow list. Fixes argoproj#1080 and ar…
Browse files Browse the repository at this point in the history
  • Loading branch information
alexec committed Apr 28, 2020
1 parent e0ad7de commit 56143eb
Show file tree
Hide file tree
Showing 14 changed files with 189 additions and 295 deletions.
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,26 +1,27 @@
import {Page, SlidingPanel} from 'argo-ui';

import * as classNames from 'classnames';
import {isNaN} from 'formik';
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 {Loading} from '../../../shared/components/loading';
import {PaginationPanel} from '../../../shared/components/pagination-panel';
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, wfDuration} from '../../../shared/duration';
import {exampleWorkflow} from '../../../shared/examples';
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';

interface State {
offset: number;
nextOffset: number;
pagination: Pagination;
loading: boolean;
initialized: boolean;
managedNamespace: boolean;
Expand All @@ -42,8 +43,7 @@ export class ArchivedWorkflowList extends BasePage<RouteComponentProps<any>, Sta
super(props, context);
this.state = {
loading: true,
offset: this.parseOffset(this.queryParam('continue') || ''),
nextOffset: 0,
pagination: {offset: this.queryParam('offset'), limit: parseLimit(this.queryParam('limit'))},
initialized: false,
managedNamespace: false,
namespace: this.props.match.params.namespace || Utils.getCurrentNamespace() || '',
Expand All @@ -61,7 +61,7 @@ export class ArchivedWorkflowList extends BasePage<RouteComponentProps<any>, Sta
this.state.selectedLabels,
this.state.minStartedAt,
this.state.maxStartedAt,
this.state.offset
this.state.pagination
);
}

Expand Down Expand Up @@ -102,7 +102,7 @@ export class ArchivedWorkflowList extends BasePage<RouteComponentProps<any>, Sta
minStartedAt={this.state.minStartedAt}
maxStartedAt={this.state.maxStartedAt}
onChange={(namespace, selectedPhases, selectedLabels, minStartedAt, maxStartedAt) =>
this.changeFilters(namespace, selectedPhases, selectedLabels, minStartedAt, maxStartedAt, 0)
this.changeFilters(namespace, selectedPhases, selectedLabels, minStartedAt, maxStartedAt, {limit: this.state.pagination.limit})
}
/>
</div>
Expand Down Expand Up @@ -146,15 +146,7 @@ export class ArchivedWorkflowList extends BasePage<RouteComponentProps<any>, Sta
}
}

private parseOffset(str: string) {
if (isNaN(str)) {
return 0;
}
const result = parseInt(str, 10);
return result >= 0 ? result : 0;
}

private changeFilters(namespace: string, selectedPhases: string[], selectedLabels: string[], minStartedAt: Date, maxStartedAt: Date, offset: number) {
private changeFilters(namespace: string, selectedPhases: string[], selectedLabels: string[], minStartedAt: Date, maxStartedAt: Date, pagination: Pagination) {
const params = new URLSearchParams();
selectedPhases.forEach(phase => {
params.append('phase', phase);
Expand All @@ -164,15 +156,18 @@ export class ArchivedWorkflowList extends BasePage<RouteComponentProps<any>, Sta
});
params.append('minStartedAt', minStartedAt.toISOString());
params.append('maxStartedAt', maxStartedAt.toISOString());
if (offset > 0) {
params.append('continue', offset.toString());
if (pagination.offset) {
params.append('offset', pagination.offset);
}
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, offset && offset >= 0 ? offset : 0);
this.fetchArchivedWorkflows(namespace, selectedPhases, selectedLabels, minStartedAt, maxStartedAt, pagination);
}

private fetchArchivedWorkflows(namespace: string, selectedPhases: string[], selectedLabels: string[], minStartedAt: Date, maxStartedAt: Date, offset: number): void {
private fetchArchivedWorkflows(namespace: string, selectedPhases: string[], selectedLabels: string[], minStartedAt: Date, maxStartedAt: Date, pagination: Pagination): void {
let archivedWorkflowList;
let newNamespace = namespace;
if (!this.state.initialized) {
Expand All @@ -181,13 +176,13 @@ export class ArchivedWorkflowList extends BasePage<RouteComponentProps<any>, Sta
newNamespace = info.managedNamespace;
}
this.setState({initialized: true, managedNamespace: !!info.managedNamespace});
return services.archivedWorkflows.list(newNamespace, selectedPhases, selectedLabels, minStartedAt, maxStartedAt, offset);
return services.archivedWorkflows.list(newNamespace, selectedPhases, selectedLabels, minStartedAt, maxStartedAt, pagination);
});
} else {
if (this.state.managedNamespace) {
newNamespace = this.state.namespace;
}
archivedWorkflowList = services.archivedWorkflows.list(newNamespace, selectedPhases, selectedLabels, minStartedAt, maxStartedAt, offset);
archivedWorkflowList = services.archivedWorkflows.list(newNamespace, selectedPhases, selectedLabels, minStartedAt, maxStartedAt, pagination);
}
archivedWorkflowList
.then(list => {
Expand All @@ -198,14 +193,18 @@ export class ArchivedWorkflowList extends BasePage<RouteComponentProps<any>, Sta
selectedLabels,
minStartedAt,
maxStartedAt,
offset,
nextOffset: this.parseOffset(list.metadata.continue || ''),
pagination: {
limit: pagination.limit,
offset: pagination.offset,
nextOffset: list.metadata.continue
},
loading: false
});
Utils.setCurrentNamespace(newNamespace);
})
.catch(error => this.setState({error, loading: false}));
}

private renderWorkflows() {
if (!this.state.workflows) {
return <Loading />;
Expand All @@ -225,50 +224,32 @@ export class ArchivedWorkflowList extends BasePage<RouteComponentProps<any>, Sta
<div className='argo-table-list'>
<div className='row argo-table-list__head'>
<div className='columns small-1' />
<div className='columns small-5'>NAME</div>
<div className='columns small-4'>NAME</div>
<div className='columns small-3'>NAMESPACE</div>
<div className='columns small-3'>CREATED</div>
<div className='columns small-2'>STARTED</div>
<div className='columns small-2'>DURATION</div>
</div>
{this.state.workflows.map(w => (
<Link className='row argo-table-list__row' key={`${w.metadata.uid}`} to={uiUrl(`archived-workflows/${w.metadata.namespace}/${w.metadata.uid}`)}>
<div className='columns small-1'>
<i className={classNames('fa', Utils.statusIconClasses(w.status.phase))} />
</div>
<div className='columns small-5'>{w.metadata.name}</div>
<div className='columns small-4'>{w.metadata.name}</div>
<div className='columns small-3'>{w.metadata.namespace}</div>
<div className='columns small-3'>
<Timestamp date={w.metadata.creationTimestamp} />
<div className='columns small-2'>
<Timestamp date={w.status.startedAt} />
<Timestamp date={w.status.finishedAt} />
</div>
<div className='columns small-2'>{formatDuration(wfDuration(w.status))}</div>
</Link>
))}
</div>
<p>
{this.state.offset !== 0 && (
<button
className='argo-button argo-button--base-o'
onClick={() => {
this.changeFilters(this.state.namespace, this.state.selectedPhases, this.state.selectedLabels, this.state.minStartedAt, this.state.maxStartedAt, 0);
}}>
<i className='fa fa-chevron-left' /> Start
</button>
)}
{this.state.nextOffset !== 0 && (
<button
className='argo-button argo-button--base-o'
onClick={() => {
this.changeFilters(
this.state.namespace,
this.state.selectedPhases,
this.state.selectedLabels,
this.state.minStartedAt,
this.state.maxStartedAt,
this.state.nextOffset
);
}}>
Next: {this.state.nextOffset} <i className='fa fa-chevron-right' />
</button>
)}
</p>
<PaginationPanel
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>
<i className='fa fa-info-circle' /> Records are created in the archive when a workflow completes. {learnMore}.
</p>
Expand Down
46 changes: 46 additions & 0 deletions ui/src/app/shared/components/pagination-panel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import * as React from 'react';
import {Pagination, parseLimit} from '../pagination';

export class PaginationPanel extends React.Component<{pagination: Pagination; onChange: (pagination: Pagination) => void}> {
public render() {
return (
<p>
<button
disabled={!this.props.pagination.offset}
className='argo-button argo-button--base-o'
onClick={() => this.props.onChange({limit: this.props.pagination.limit})}>
First page
</button>
<button
disabled={!this.props.pagination.nextOffset}
className='argo-button argo-button--base-o'
onClick={() =>
this.props.onChange({
limit: this.props.pagination.limit,
offset: this.props.pagination.nextOffset
})
}>
Next page <i className='fa fa-chevron-right' />
</button>
<small className='fa-pull-right'>
<select
className='small'
onChange={e =>
this.props.onChange({
offset: this.props.pagination.offset,
limit: parseLimit(e.target.value)
})
}
value={this.props.pagination.limit}>
{[5, 10, 20, 50, 100, 500].map(limit => (
<option key={limit} value={limit}>
{limit}
</option>
))}
</select>{' '}
results per page
</small>
</p>
);
}
}
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;
}
14 changes: 14 additions & 0 deletions ui/src/app/shared/pagination.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {isNaN} from 'formik';

export interface Pagination {
offset?: string;
limit: number;
nextOffset?: string;
}

export const defaultPaginationLimit = 10;

export function parseLimit(str: string) {
const v = parseInt(str, 10);
return isNaN(v) ? defaultPaginationLimit : v;
}
12 changes: 7 additions & 5 deletions ui/src/app/shared/services/archived-workflows-service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import * as models from '../../../models';
import {Pagination} from '../pagination';
import requests from './requests';

export class ArchivedWorkflowsService {
public list(namespace: string, phases: string[], labels: string[], minStartedAt: Date, maxStartedAt: Date, offset: number) {
public list(namespace: string, phases: string[], labels: string[], minStartedAt: Date, maxStartedAt: Date, pagination: Pagination) {
return requests
.get(`api/v1/archived-workflows?${this.queryParams({namespace, phases, labels, minStartedAt, maxStartedAt, offset}).join('&')}`)
.get(`api/v1/archived-workflows?${this.queryParams({namespace, phases, labels, minStartedAt, maxStartedAt, pagination}).join('&')}`)
.then(res => res.body as models.WorkflowList);
}

Expand All @@ -16,7 +17,7 @@ export class ArchivedWorkflowsService {
return requests.delete(`api/v1/archived-workflows/${uid}`);
}

private queryParams(filter: {namespace?: string; phases?: Array<string>; labels?: Array<string>; minStartedAt?: Date; maxStartedAt?: Date; offset?: number}) {
private queryParams(filter: {namespace?: string; phases?: Array<string>; labels?: Array<string>; minStartedAt?: Date; maxStartedAt?: Date; pagination: Pagination}) {
const queryParams: string[] = [];
const fieldSelector = this.fieldSelectorParams(filter.namespace, filter.minStartedAt, filter.maxStartedAt);
if (fieldSelector.length > 0) {
Expand All @@ -26,9 +27,10 @@ export class ArchivedWorkflowsService {
if (labelSelector.length > 0) {
queryParams.push(`listOptions.labelSelector=${labelSelector}`);
}
if (filter.offset) {
queryParams.push(`listOptions.continue=${filter.offset}`);
if (filter.pagination.offset) {
queryParams.push(`listOptions.continue=${filter.pagination.offset}`);
}
queryParams.push(`listOptions.limit=${filter.pagination.limit}`);
return queryParams;
}

Expand Down
11 changes: 9 additions & 2 deletions ui/src/app/shared/services/workflows-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {Observable, Observer} from 'rxjs';
import {catchError, map} from 'rxjs/operators';
import * as models from '../../../models';
import {Workflow, WorkflowList} from '../../../models';
import {Pagination} from '../pagination';
import requests from './requests';
import {WorkflowDeleteResponse} from './responses';

Expand All @@ -14,8 +15,14 @@ export class WorkflowsService {
.then(res => res.body as Workflow);
}

public list(namespace: string, phases: string[], labels: string[]) {
return requests.get(`api/v1/workflows/${namespace}?${this.queryParams({phases, labels}).join('&')}`).then(res => res.body as WorkflowList);
public list(namespace: string, phases: string[], labels: string[], pagination: Pagination) {
const params = this.queryParams({phases, labels});
if (pagination.offset) {
params.push(`listOptions.continue=${pagination.offset}`);
}
params.push(`listOptions.limit=${pagination.limit}`);
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) {
Expand Down
1 change: 0 additions & 1 deletion ui/src/app/workflows/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export * from './workflow-dag/workflow-dag';
export * from './workflow-list-item/workflow-list-item';
export * from './workflow-logs-viewer/workflow-logs-viewer';
export * from './workflow-node-info/workflow-node-info';
export * from './workflow-timeline/workflow-timeline';
Expand Down
Loading

0 comments on commit 56143eb

Please sign in to comment.