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 all commits
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
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 => (
Copy link
Member

Choose a reason for hiding this comment

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

Nice

<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