Skip to content

Commit ad4cae1

Browse files
authored
Workflows backend filtering/sorting - UX (#584)
* Workflows backend filtering/sorting
1 parent 6f183cc commit ad4cae1

File tree

11 files changed

+291
-109
lines changed

11 files changed

+291
-109
lines changed

ui/src/app/components/workflows/workflows-home/workflows-home.component.html

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ <h3 class="modal-title">Upload workflow</h3>
7373
</div>
7474
</clr-modal>
7575

76-
<clr-datagrid [(clrDgSelected)]="selected" (clrDgRefresh)="onClarityDgRefresh($event)">
76+
<clr-datagrid [(clrDgSelected)]="selected" (clrDgRefresh)="onClarityDgRefresh($event)" [clrDgLoading]="loading">
7777

7878
<clr-dg-action-bar>
7979
<clr-dropdown>
@@ -170,7 +170,7 @@ <h3 class="modal-title">Upload workflow</h3>
170170

171171
</clr-dg-column>
172172

173-
<clr-dg-row *clrDgItems="let workflow of workflows" [clrDgItem]="workflow">
173+
<clr-dg-row *ngFor="let workflow of workflows" [clrDgItem]="workflow">
174174
<clr-dg-action-overflow>
175175
<button type="button" class="action-item" (click)="showWorkflow(workflow.id)">
176176
<clr-icon shape="eye"></clr-icon>
@@ -218,5 +218,12 @@ <h3 class="modal-title">Upload workflow</h3>
218218
{{workflow.isActive ? 'Yes' : 'No'}}
219219
</clr-dg-cell>
220220
</clr-dg-row>
221+
<clr-dg-footer>
222+
<clr-dg-pagination #pagination [clrDgPageSize]="pageSize" [clrDgPage]="page"
223+
[clrDgTotalItems]="total"></clr-dg-pagination>
224+
<clr-dg-page-size [clrPageSizeOptions]="[50,100,150,200,250,300]">Workflows per page</clr-dg-page-size>
225+
{{pagination.firstItem + 1}} - {{pagination.lastItem + 1}}
226+
of {{total}} workflows
227+
</clr-dg-footer>
221228
</clr-datagrid>
222229

ui/src/app/components/workflows/workflows-home/workflows-home.component.spec.ts

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
1717

1818
import { WorkflowsHomeComponent } from './workflows-home.component';
1919
import { provideMockStore } from '@ngrx/store/testing';
20-
import { ProjectModelFactory } from '../../../models/project.model';
2120
import { WorkflowModelFactory } from '../../../models/workflow.model';
2221
import { ConfirmationDialogService } from '../../../services/confirmation-dialog/confirmation-dialog.service';
2322
import { Store } from '@ngrx/store';
@@ -30,12 +29,12 @@ import { ClrDatagridStateInterface } from '@clr/angular';
3029
import {
3130
DeleteWorkflow,
3231
SwitchWorkflowActiveState,
33-
SetWorkflowsSort,
3432
LoadJobsForRun,
3533
ExportWorkflows,
3634
SetWorkflowFile,
3735
ImportWorkflows,
3836
RunWorkflows,
37+
SearchWorkflows,
3938
} from '../../../stores/workflows/workflows.actions';
4039

4140
describe('WorkflowsHomeComponent', () => {
@@ -47,13 +46,19 @@ describe('WorkflowsHomeComponent', () => {
4746

4847
const initialAppState = {
4948
workflows: {
50-
workflows: [
51-
WorkflowModelFactory.create('workflowOne', undefined, undefined, undefined, undefined, undefined),
52-
WorkflowModelFactory.create('workflowTwo', undefined, undefined, undefined, undefined, undefined),
53-
],
49+
workflowsSearch: {
50+
loading: true,
51+
workflows: [
52+
WorkflowModelFactory.create('workflowOne', undefined, undefined, undefined, undefined, undefined),
53+
WorkflowModelFactory.create('workflowTwo', undefined, undefined, undefined, undefined, undefined),
54+
],
55+
total: 2,
56+
searchRequest: undefined,
57+
},
58+
workflowAction: {
59+
loading: false,
60+
},
5461
},
55-
workflowsSort: undefined,
56-
workflowsFilters: undefined,
5762
};
5863

5964
beforeEach(
@@ -83,9 +88,13 @@ describe('WorkflowsHomeComponent', () => {
8388
waitForAsync(() => {
8489
fixture.detectChanges();
8590
fixture.whenStable().then(() => {
86-
expect(underTest.workflows).toEqual([...initialAppState.workflows.workflows]);
87-
expect(underTest.sort).toEqual(initialAppState.workflowsSort);
88-
expect(underTest.filters).toBeUndefined();
91+
expect(underTest.workflows).toEqual([...initialAppState.workflows.workflowsSearch.workflows]);
92+
expect(underTest.total).toEqual(initialAppState.workflows.workflowsSearch.total);
93+
expect(underTest.sort).toEqual(undefined);
94+
expect(underTest.filters).toEqual([]);
95+
expect(underTest.pageFrom).toEqual(0);
96+
expect(underTest.pageSize).toEqual(100);
97+
expect(underTest.page).toEqual(0 / 100 + 1);
8998
});
9099
}),
91100
);
@@ -291,7 +300,6 @@ describe('WorkflowsHomeComponent', () => {
291300
subject.next(true);
292301

293302
fixture.detectChanges();
294-
expect(underTest.ignoreRefresh).toBeTrue();
295303
fixture.whenStable().then(() => {
296304
expect(storeSpy).toHaveBeenCalled();
297305
expect(storeSpy).toHaveBeenCalledWith(new DeleteWorkflow(id));
@@ -312,7 +320,6 @@ describe('WorkflowsHomeComponent', () => {
312320
subject.next(false);
313321

314322
fixture.detectChanges();
315-
expect(underTest.ignoreRefresh).toBeTrue();
316323
fixture.whenStable().then(() => {
317324
expect(storeSpy).toHaveBeenCalledTimes(0);
318325
});
@@ -333,7 +340,6 @@ describe('WorkflowsHomeComponent', () => {
333340
subject.next(true);
334341

335342
fixture.detectChanges();
336-
expect(underTest.ignoreRefresh).toBeTrue();
337343
fixture.whenStable().then(() => {
338344
expect(storeSpy).toHaveBeenCalled();
339345
expect(storeSpy).toHaveBeenCalledWith(
@@ -360,7 +366,6 @@ describe('WorkflowsHomeComponent', () => {
360366
subject.next(false);
361367

362368
fixture.detectChanges();
363-
expect(underTest.ignoreRefresh).toBeFalse();
364369
fixture.whenStable().then(() => {
365370
expect(storeSpy).toHaveBeenCalledTimes(0);
366371
});
@@ -444,31 +449,31 @@ describe('WorkflowsHomeComponent', () => {
444449

445450
describe('onClarityDgRefresh', () => {
446451
it(
447-
'should dispatch SetWorkflowsSort when ignoreRefresh is false',
452+
'should dispatch SearchWorkflows when ignoreRefresh is false',
448453
waitForAsync(() => {
449454
underTest.ignoreRefresh = false;
455+
underTest.loading = true;
456+
underTest.filters = [];
450457

451458
const subject = new Subject<boolean>();
452459
const storeSpy = spyOn(store, 'dispatch');
453-
const state: ClrDatagridStateInterface = {};
454460

455-
underTest.onClarityDgRefresh(state);
461+
underTest.refresh();
456462
subject.next(true);
457463

458464
fixture.detectChanges();
459-
expect(underTest.ignoreRefresh).toBeFalse();
460-
expect(underTest.sort).toBeUndefined();
461-
expect(underTest.filters).toBeUndefined();
462465

463466
fixture.whenStable().then(() => {
464467
expect(storeSpy).toHaveBeenCalled();
465-
expect(storeSpy).toHaveBeenCalledWith(new SetWorkflowsSort(underTest.sort));
468+
expect(storeSpy).toHaveBeenCalledWith(
469+
new SearchWorkflows({ from: 0, size: 0, sort: undefined, containsFilterAttributes: [], booleanFilterAttributes: [] }),
470+
);
466471
});
467472
}),
468473
);
469474

470475
it(
471-
'onClarityDgRefresh() should not dispatch SetWorkflowsSort and SetWorkflowsFilters when ignoreRefresh is true',
476+
'onClarityDgRefresh() should not dispatch SearchWorkflows when ignoreRefresh is true',
472477
waitForAsync(() => {
473478
underTest.ignoreRefresh = true;
474479

ui/src/app/components/workflows/workflows-home/workflows-home.component.ts

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* limitations under the License.
1414
*/
1515

16-
import { Component, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core';
16+
import { AfterViewInit, Component, OnDestroy, OnInit, QueryList, ViewChildren } from '@angular/core';
1717
import { Subject, Subscription } from 'rxjs';
1818
import { AppState, selectWorkflowState } from '../../../stores/app.reducers';
1919
import { WorkflowModel } from '../../../models/workflow.model';
@@ -24,9 +24,8 @@ import {
2424
ImportWorkflows,
2525
LoadJobsForRun,
2626
RunWorkflows,
27+
SearchWorkflows,
2728
SetWorkflowFile,
28-
SetWorkflowsFilters,
29-
SetWorkflowsSort,
3029
UpdateWorkflowsIsActive,
3130
} from '../../../stores/workflows/workflows.actions';
3231
import { ConfirmationDialogTypes } from '../../../constants/confirmationDialogTypes.constants';
@@ -38,24 +37,36 @@ import { ClrDatagridColumn, ClrDatagridStateInterface } from '@clr/angular';
3837
import { SortAttributesModel } from '../../../models/search/sortAttributes.model';
3938
import { filter } from 'rxjs/operators';
4039
import { workflowsHomeColumns } from 'src/app/constants/workflow.constants';
40+
import { TableSearchRequestModel } from '../../../models/search/tableSearchRequest.model';
41+
import { ContainsFilterAttributes } from '../../../models/search/containsFilterAttributes.model';
42+
import { BooleanFilterAttributes } from '../../../models/search/booleanFilterAttributes.model';
4143

4244
@Component({
4345
selector: 'app-workflows-home',
4446
templateUrl: './workflows-home.component.html',
4547
styleUrls: ['./workflows-home.component.scss'],
4648
})
47-
export class WorkflowsHomeComponent implements OnInit, OnDestroy {
49+
export class WorkflowsHomeComponent implements OnInit, AfterViewInit, OnDestroy {
4850
@ViewChildren(ClrDatagridColumn) columns: QueryList<ClrDatagridColumn>;
4951

5052
confirmationDialogServiceSubscription: Subscription = null;
5153
runWorkflowDialogSubscription: Subscription = null;
5254
workflowsSubscription: Subscription = null;
55+
loadingSubscription: Subscription = null;
5356
routerSubscription: Subscription = null;
5457
workflows: WorkflowModel[] = [];
5558
absoluteRoutes = absoluteRoutes;
5659
workflowsHomeColumns = workflowsHomeColumns;
5760
selected: WorkflowModel[] = [];
5861

62+
loading = true;
63+
loadingAction = false;
64+
65+
page = 1;
66+
total = 0;
67+
pageFrom = 0;
68+
pageSize = 0;
69+
5970
removeWorkflowFilterSubject: Subject<any> = new Subject();
6071
sort: SortAttributesModel = undefined;
6172
filters: any[] = undefined;
@@ -74,9 +85,29 @@ export class WorkflowsHomeComponent implements OnInit, OnDestroy {
7485

7586
ngOnInit(): void {
7687
this.workflowsSubscription = this.store.select(selectWorkflowState).subscribe((state) => {
77-
this.workflows = state.workflows;
78-
this.sort = state.workflowsSort;
79-
this.filters = state.workflowsFilters;
88+
this.workflows = state.workflowsSearch.workflows;
89+
this.total = state.workflowsSearch.total;
90+
this.sort = state.workflowsSearch.searchRequest?.sort;
91+
this.filters = [
92+
...(state.workflowsSearch.searchRequest?.containsFilterAttributes || []),
93+
...(state.workflowsSearch.searchRequest?.booleanFilterAttributes || []),
94+
];
95+
this.pageFrom = state.workflowsSearch.searchRequest?.from ? state.workflowsSearch.searchRequest?.from : 0;
96+
this.pageSize = state.workflowsSearch.searchRequest?.size ? state.workflowsSearch.searchRequest?.size : 100;
97+
this.page = this.pageFrom / this.pageSize + 1;
98+
99+
if (this.loadingAction == true && state.workflowAction.loading == false) {
100+
this.loadingAction = false;
101+
this.refresh();
102+
} else {
103+
this.loadingAction = state.workflowAction.loading;
104+
}
105+
});
106+
}
107+
108+
ngAfterViewInit(): void {
109+
this.loadingSubscription = this.store.select(selectWorkflowState).subscribe((state) => {
110+
this.loading = state.workflowsSearch.loading;
80111
});
81112
}
82113

@@ -95,7 +126,6 @@ export class WorkflowsHomeComponent implements OnInit, OnDestroy {
95126
this.confirmationDialogServiceSubscription = this.confirmationDialogService
96127
.confirm(ConfirmationDialogTypes.YesOrNo, texts.BULK_RUN_WORKFLOWS_TITLE, texts.BULK_RUN_WORKFLOWS_CONTENT(selected.length))
97128
.subscribe((confirmed) => {
98-
this.ignoreRefresh = true;
99129
if (confirmed) this.store.dispatch(new RunWorkflows(selected.map((workflow) => workflow.id)));
100130
});
101131
}
@@ -146,7 +176,6 @@ export class WorkflowsHomeComponent implements OnInit, OnDestroy {
146176
this.confirmationDialogServiceSubscription = this.confirmationDialogService
147177
.confirm(ConfirmationDialogTypes.Delete, texts.DELETE_WORKFLOW_CONFIRMATION_TITLE, texts.DELETE_WORKFLOW_CONFIRMATION_CONTENT)
148178
.subscribe((confirmed) => {
149-
this.ignoreRefresh = true;
150179
if (confirmed) this.store.dispatch(new DeleteWorkflow(id));
151180
});
152181
}
@@ -160,7 +189,6 @@ export class WorkflowsHomeComponent implements OnInit, OnDestroy {
160189
)
161190
.subscribe((confirmed) => {
162191
if (confirmed) {
163-
this.ignoreRefresh = true;
164192
this.store.dispatch(new SwitchWorkflowActiveState({ id: id, currentActiveState: currentActiveState }));
165193
}
166194
});
@@ -177,16 +205,28 @@ export class WorkflowsHomeComponent implements OnInit, OnDestroy {
177205
onClarityDgRefresh(state: ClrDatagridStateInterface) {
178206
if (!this.ignoreRefresh) {
179207
this.sort = state.sort ? new SortAttributesModel(state.sort.by as string, state.sort.reverse ? -1 : 1) : undefined;
180-
this.store.dispatch(new SetWorkflowsSort(this.sort));
208+
this.pageFrom = state.page.from < 0 ? 0 : state.page.from;
209+
this.pageSize = state.page.size;
181210
this.filters = state.filters ? state.filters : [];
182-
this.store.dispatch(new SetWorkflowsFilters(this.filters));
211+
this.refresh();
183212
}
184213
}
185214

215+
refresh() {
216+
const searchRequestModel: TableSearchRequestModel = {
217+
from: this.pageFrom,
218+
size: this.pageSize,
219+
sort: this.sort,
220+
containsFilterAttributes: this.filters.filter((f) => f instanceof ContainsFilterAttributes).map((f) => f as ContainsFilterAttributes),
221+
booleanFilterAttributes: this.filters.filter((f) => f instanceof BooleanFilterAttributes).map((f) => f as BooleanFilterAttributes),
222+
};
223+
this.store.dispatch(new SearchWorkflows(searchRequestModel));
224+
}
225+
186226
getFilter(name: string): any | undefined {
187227
let filter = undefined;
188228
if (this.filters) {
189-
filter = this.filters.find((filter) => filter.field == name);
229+
filter = this.filters.find((filter) => filter?.field == name);
190230
}
191231

192232
return filter && filter.value ? filter.value : undefined;
@@ -197,7 +237,8 @@ export class WorkflowsHomeComponent implements OnInit, OnDestroy {
197237
}
198238

199239
clearFilters() {
200-
this.removeWorkflowFilterSubject.next();
240+
this.filters = [];
241+
this.refresh();
201242
}
202243

203244
clearSort() {
@@ -236,7 +277,6 @@ export class WorkflowsHomeComponent implements OnInit, OnDestroy {
236277
)
237278
.subscribe((confirmed) => {
238279
if (confirmed) {
239-
this.ignoreRefresh = true;
240280
this.store.dispatch(new UpdateWorkflowsIsActive({ ids: ids, isActiveNewValue: isActiveNewValue }));
241281
}
242282
});
@@ -247,5 +287,6 @@ export class WorkflowsHomeComponent implements OnInit, OnDestroy {
247287
!!this.confirmationDialogServiceSubscription && this.confirmationDialogServiceSubscription.unsubscribe();
248288
!!this.runWorkflowDialogSubscription && this.runWorkflowDialogSubscription.unsubscribe();
249289
!!this.routerSubscription && this.routerSubscription.unsubscribe();
290+
!!this.loadingSubscription && this.loadingSubscription.unsubscribe();
250291
}
251292
}

ui/src/app/constants/api.constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const api = {
2525

2626
GET_PROJECTS: '/workflows/projects',
2727
GET_WORKFLOWS: '/workflows',
28+
SEARCH_WORKFLOWS: '/workflows/search',
2829
GET_WORKFLOW: '/workflow',
2930
DELETE_WORKFLOW: '/workflows',
3031
EXPORT_WORKFLOWS: '/workflows/export',

ui/src/app/constants/texts.constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
export const texts = {
1717
LOAD_WORKFLOWS_FAILURE_NOTIFICATION: "Sorry, workflows couldn't be loaded. Please try again.",
18+
SEARCH_WORKFLOWS_FAILURE_NOTIFICATION: "Sorry, workflows couldn't be loaded. Please try again.",
1819

1920
DELETE_WORKFLOW_CONFIRMATION_TITLE: 'Delete workflow',
2021
DELETE_WORKFLOW_CONFIRMATION_CONTENT: 'Are you sure you want to delete this workflow? The operation cannot be reverted.',

ui/src/app/services/workflow/workflow.service.spec.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/
1919
import { api } from '../../constants/api.constants';
2020
import { WorkflowService } from './workflow.service';
2121
import { ProjectModelFactory } from '../../models/project.model';
22-
import { WorkflowModelFactory } from '../../models/workflow.model';
22+
import { WorkflowModel, WorkflowModelFactory } from '../../models/workflow.model';
2323
import { WorkflowJoinedModelFactory } from '../../models/workflowJoined.model';
24+
import { TableSearchResponseModel } from '../../models/search/tableSearchResponse.model';
25+
import { TableSearchRequestModelFactory } from '../../models/search/tableSearchRequest.model';
2426

2527
describe('WorkflowService', () => {
2628
let underTest: WorkflowService;
@@ -73,6 +75,21 @@ describe('WorkflowService', () => {
7375
req.flush([...workflows]);
7476
});
7577

78+
it('searchWorkflows() should return workflows search response', () => {
79+
const workflows = [WorkflowModelFactory.create('workflowName1', true, 'projectName1', new Date(Date.now()), new Date(Date.now()), 0)];
80+
const searchResponseModel = new TableSearchResponseModel<WorkflowModel>(workflows, 1);
81+
const request = TableSearchRequestModelFactory.create(0, 100);
82+
83+
underTest.searchWorkflows(request).subscribe(
84+
(data) => expect(data).toEqual(searchResponseModel),
85+
(error) => fail(error),
86+
);
87+
88+
const req = httpTestingController.expectOne(api.SEARCH_WORKFLOWS);
89+
expect(req.request.method).toEqual('POST');
90+
req.flush(searchResponseModel);
91+
});
92+
7693
it('getWorkflow() should return workflow data', () => {
7794
const workflow = WorkflowJoinedModelFactory.create('name', true, 'project', undefined, undefined, undefined, 0);
7895

0 commit comments

Comments
 (0)