From 3303611bcc6671aba1e49ceb3b9317ca5e718735 Mon Sep 17 00:00:00 2001 From: bbortt Date: Mon, 13 Nov 2023 14:59:02 +0100 Subject: [PATCH] feat(simulator-ui): scenario execution filter --- .../message-header-detail.component.html | 2 +- .../list/message-header-table.component.html | 2 +- .../list/message-header-table.component.ts | 2 +- .../list/message-header.component.html | 2 +- .../list/message-header.component.ts | 6 +- .../detail/message-detail.component.html | 2 +- .../message/list/message.component.html | 6 +- .../message/list/message.component.ts | 8 +- .../list/scenario-action.component.html | 2 +- .../list/scenario-action.component.ts | 7 +- .../service/scenario-action.service.ts | 8 -- .../scenario-execution-detail.component.html | 2 +- .../list/scenario-execution.component.html | 12 +- .../list/scenario-execution.component.ts | 12 +- .../scenario-execution.model.spec.ts | 36 +++++ .../scenario-execution.model.ts | 20 +++ .../scenario-parameter-detail.component.html | 2 +- .../list/scenario-parameter.component.html | 4 +- .../list/scenario-parameter.component.ts | 8 +- .../service/scenario-parameter.service.ts | 7 - .../test-parameter-detail.component.html | 2 +- .../list/test-parameter.component.html | 4 +- .../list/test-parameter.component.ts | 8 +- .../detail/test-result-detail.component.html | 2 +- .../list/test-result.component.html | 6 +- .../test-result/list/test-result.component.ts | 7 +- .../home/test-result-summary.component.html | 6 +- .../scenario-execution-filter.component.html | 55 ++++++++ ...cenario-execution-filter.component.spec.ts | 123 ++++++++++++++++++ .../scenario-execution-filter.component.ts | 120 +++++++++++++++++ .../scenario-result.component.html | 17 ++- .../scenario-result.component.ts | 11 +- .../detail/scenario-detail.component.html | 2 +- .../app/scenario/list/scenario.component.html | 4 +- .../format-date-time-filter-options.spec.ts | 61 +++++++++ .../date/format-date-time-filter-options.ts | 19 +++ .../webapp/i18n/en/scenarioExecution.json | 9 ++ 37 files changed, 547 insertions(+), 59 deletions(-) create mode 100644 simulator-ui/src/main/webapp/app/entities/scenario-execution/scenario-execution.model.spec.ts create mode 100644 simulator-ui/src/main/webapp/app/scenario-result/scenario-execution-filter.component.html create mode 100644 simulator-ui/src/main/webapp/app/scenario-result/scenario-execution-filter.component.spec.ts create mode 100644 simulator-ui/src/main/webapp/app/scenario-result/scenario-execution-filter.component.ts create mode 100644 simulator-ui/src/main/webapp/app/shared/date/format-date-time-filter-options.spec.ts create mode 100644 simulator-ui/src/main/webapp/app/shared/date/format-date-time-filter-options.ts diff --git a/simulator-ui/src/main/webapp/app/entities/message-header/detail/message-header-detail.component.html b/simulator-ui/src/main/webapp/app/entities/message-header/detail/message-header-detail.component.html index 88e142171..f74352606 100644 --- a/simulator-ui/src/main/webapp/app/entities/message-header/detail/message-header-detail.component.html +++ b/simulator-ui/src/main/webapp/app/entities/message-header/detail/message-header-detail.component.html @@ -40,7 +40,7 @@

- diff --git a/simulator-ui/src/main/webapp/app/entities/message-header/list/message-header-table.component.html b/simulator-ui/src/main/webapp/app/entities/message-header/list/message-header-table.component.html index 9a88b266f..1343c5c59 100644 --- a/simulator-ui/src/main/webapp/app/entities/message-header/list/message-header-table.component.html +++ b/simulator-ui/src/main/webapp/app/entities/message-header/list/message-header-table.component.html @@ -58,7 +58,7 @@
diff --git a/simulator-ui/src/main/webapp/app/entities/message/list/message.component.html b/simulator-ui/src/main/webapp/app/entities/message/list/message.component.html index 72497a6b0..51204fa8c 100644 --- a/simulator-ui/src/main/webapp/app/entities/message/list/message.component.html +++ b/simulator-ui/src/main/webapp/app/entities/message/list/message.component.html @@ -14,7 +14,7 @@

- +
No Messages found @@ -87,7 +87,7 @@

diff --git a/simulator-ui/src/main/webapp/app/entities/scenario-execution/list/scenario-execution.component.html b/simulator-ui/src/main/webapp/app/entities/scenario-execution/list/scenario-execution.component.html index e78f7461a..fd5016edf 100644 --- a/simulator-ui/src/main/webapp/app/entities/scenario-execution/list/scenario-execution.component.html +++ b/simulator-ui/src/main/webapp/app/entities/scenario-execution/list/scenario-execution.component.html @@ -1,5 +1,5 @@
-

+

Scenario Executions
@@ -14,7 +14,7 @@

- +
No Scenario Executions found @@ -80,7 +80,7 @@

diff --git a/simulator-ui/src/main/webapp/app/entities/scenario-parameter/list/scenario-parameter.component.html b/simulator-ui/src/main/webapp/app/entities/scenario-parameter/list/scenario-parameter.component.html index 94c2437a2..49090500d 100644 --- a/simulator-ui/src/main/webapp/app/entities/scenario-parameter/list/scenario-parameter.component.html +++ b/simulator-ui/src/main/webapp/app/entities/scenario-parameter/list/scenario-parameter.component.html @@ -14,7 +14,7 @@

- +
No Scenario Parameters found @@ -89,7 +89,7 @@

diff --git a/simulator-ui/src/main/webapp/app/entities/test-parameter/list/test-parameter.component.html b/simulator-ui/src/main/webapp/app/entities/test-parameter/list/test-parameter.component.html index 0b13fcf27..63f35fcb2 100644 --- a/simulator-ui/src/main/webapp/app/entities/test-parameter/list/test-parameter.component.html +++ b/simulator-ui/src/main/webapp/app/entities/test-parameter/list/test-parameter.component.html @@ -14,7 +14,7 @@

- +
No Test Parameters found @@ -69,7 +69,7 @@

diff --git a/simulator-ui/src/main/webapp/app/entities/test-result/list/test-result.component.html b/simulator-ui/src/main/webapp/app/entities/test-result/list/test-result.component.html index 027d581b9..8f551b013 100644 --- a/simulator-ui/src/main/webapp/app/entities/test-result/list/test-result.component.html +++ b/simulator-ui/src/main/webapp/app/entities/test-result/list/test-result.component.html @@ -14,7 +14,7 @@

- +
No Test Results found @@ -101,7 +101,7 @@

{{ testResults?.total ?? 0 }} (100 %)
-
+ +
+

+ +

diff --git a/simulator-ui/src/main/webapp/app/scenario-result/scenario-execution-filter.component.spec.ts b/simulator-ui/src/main/webapp/app/scenario-result/scenario-execution-filter.component.spec.ts new file mode 100644 index 000000000..bb8df105d --- /dev/null +++ b/simulator-ui/src/main/webapp/app/scenario-result/scenario-execution-filter.component.spec.ts @@ -0,0 +1,123 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, convertToParamMap, Router } from '@angular/router'; + +import { of } from 'rxjs'; + +import ScenarioExecutionFilterComponent from './scenario-execution-filter.component'; +import { STATUS_SUCCESS } from '../entities/scenario-execution/scenario-execution.model'; + +const queryParamStartDate = new Date(2023, 10, 15).toISOString(); +const queryParamEndDate = new Date(2023, 10, 16).toISOString(); + +const filterFormFromDate = '2023-11-15 00:00:00'; +const filterFormToDate = '2023-11-16 00:00:00'; + +describe('ScenarioExecution Filter Component', () => { + let router: Router; + let activatedRoute: ActivatedRoute; + + let fixture: ComponentFixture; + let component: ScenarioExecutionFilterComponent; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ScenarioExecutionFilterComponent], + providers: [ + { + provide: Router, + useValue: { navigate: jest.fn().mockReturnValueOnce(Promise.resolve()) }, + }, + { + provide: ActivatedRoute, + useValue: { queryParamMap: of(convertToParamMap({})) }, + }, + ], + }) + .overrideTemplate(ScenarioExecutionFilterComponent, '') + .compileComponents(); + + router = TestBed.inject(Router); + activatedRoute = TestBed.inject(ActivatedRoute); + + fixture = TestBed.createComponent(ScenarioExecutionFilterComponent); + component = fixture.componentInstance; + }); + + it('should initialize form values from activated route on ngOnInit', () => { + const nameContains = 'testName'; + + // @ts-ignore: Access read-only property for testing + activatedRoute.queryParamMap = of( + convertToParamMap({ + 'filter[scenarioName.contains]': nameContains, + 'filter[startDate.greaterThanOrEqual]': queryParamStartDate, + 'filter[status.in]': STATUS_SUCCESS.id, + 'filter[endDate.lessThanOrEqual]': queryParamEndDate, + }), + ); + + component.ngOnInit(); + + expect(component.filterForm.controls.nameContains.value).toEqual(nameContains); + expect(component.filterForm.controls.fromDate.value).toEqual(filterFormFromDate); + expect(component.filterForm.controls.toDate.value).toEqual(filterFormToDate); + expect(component.filterForm.controls.statusIn.value).toEqual(STATUS_SUCCESS.name); + + expect(component.filterForm.dirty).toBeTruthy(); + expect(component.valueChanged).toBeFalsy(); + }); + + it('should navigate with correct query parameters on applyFilter', () => { + const nameContains = 'testName'; + + component.valueChanged = true; + component.filterForm.setValue({ + nameContains, + fromDate: filterFormFromDate, + toDate: filterFormToDate, + statusIn: STATUS_SUCCESS.name, + }); + + component.applyFilter(); + + expect(router.navigate).toHaveBeenCalledWith([], { + queryParams: { + 'filter[scenarioName.contains]': nameContains, + 'filter[startDate.greaterThanOrEqual]': queryParamStartDate, + 'filter[status.in]': STATUS_SUCCESS.id, + 'filter[endDate.lessThanOrEqual]': queryParamEndDate, + }, + }); + expect(component.valueChanged).toBeFalsy(); + }); + + describe('resetFilter', () => { + it('should reset form', () => { + // Assume filter is dirty + component.valueChanged = true; + component.filterForm.markAsDirty(); + + jest.spyOn(component.filterForm, 'reset'); + jest.spyOn(component.filterForm, 'markAsPristine'); + + component.resetFilter(); + + expect(component.filterForm.reset).toHaveBeenCalled(); + expect(component.filterForm.markAsPristine).toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalled(); + expect(component.valueChanged).toBeFalsy(); + }); + + it('should unsubscribe from previous filterFormValueChanges', () => { + const filterFormValueChanges = { + unsubscribe: jest.fn(), + }; + // @ts-ignore: access private property + component.filterFormValueChanges = filterFormValueChanges; + + component.resetFilter(); + + expect(filterFormValueChanges.unsubscribe).toHaveBeenCalled(); + }); + }); +}); diff --git a/simulator-ui/src/main/webapp/app/scenario-result/scenario-execution-filter.component.ts b/simulator-ui/src/main/webapp/app/scenario-result/scenario-execution-filter.component.ts new file mode 100644 index 000000000..f27c2cb7e --- /dev/null +++ b/simulator-ui/src/main/webapp/app/scenario-result/scenario-execution-filter.component.ts @@ -0,0 +1,120 @@ +import { Component, OnInit } from '@angular/core'; +import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, ParamMap, Router } from '@angular/router'; + +import dayjs from 'dayjs/esm'; + +import { + IScenarioExecutionStatus, + scenarioExecutionStatusFromId, + scenarioExecutionStatusFromName, +} from 'app/entities/scenario-execution/scenario-execution.model'; +import SharedModule from 'app/shared/shared.module'; +import { formatDateTimeFilterOptions } from 'app/shared/date/format-date-time-filter-options'; +import { FilterOptions, IFilterOptions } from 'app/shared/filter'; +import { first, Subscription } from 'rxjs'; + +type ScenarioExecutionFilter = { + nameContains: string | undefined; + fromDate: dayjs.Dayjs | undefined; + toDate: dayjs.Dayjs | undefined; + statusIn: IScenarioExecutionStatus | undefined; +}; + +@Component({ + standalone: true, + selector: 'app-scenario-execution-filter', + templateUrl: './scenario-execution-filter.component.html', + imports: [FormsModule, ReactiveFormsModule, SharedModule], +}) +export default class ScenarioExecutionFilterComponent implements OnInit { + filterForm: FormGroup = new FormGroup({ + nameContains: new FormControl(), + fromDate: new FormControl(), + toDate: new FormControl(), + statusIn: new FormControl(), + }); + valueChanged = false; + private filterFormValueChanges: Subscription | null = null; + + constructor( + private activatedRoute: ActivatedRoute, + private router: Router, + ) {} + + ngOnInit(): void { + this.activatedRoute.queryParamMap.subscribe(params => this.initializeFilterOptionsFromActivatedRoute(params)); + this.markFilterFormAsUnchanged(); + } + + applyFilter(): void { + const formValue = this.filterForm.value; + this.router + .navigate([], { + queryParams: { + ...this.getFilterQueryParameter({ + nameContains: formValue.nameContains, + fromDate: formValue.fromDate ? dayjs(formValue.fromDate) : undefined, + toDate: formValue.toDate ? dayjs(formValue.toDate) : undefined, + statusIn: formValue.statusIn ? scenarioExecutionStatusFromName(formValue.statusIn) : undefined, + }), + }, + }) + .catch(() => location.reload()); + this.markFilterFormAsUnchanged(); + } + + resetFilter(): void { + this.filterForm.reset(); + this.filterForm.markAsPristine(); + this.applyFilter(); + this.markFilterFormAsUnchanged(); + } + + private initializeFilterOptionsFromActivatedRoute(params: ParamMap): void { + let filters: IFilterOptions = new FilterOptions(); + filters.initializeFromParams(params); + filters = formatDateTimeFilterOptions(filters); + filters.filterOptions.map(filterOption => { + filterOption.name = filterOption.name.split('.')[0]; + switch (filterOption.name) { + case 'scenarioName': + this.filterForm.controls.nameContains.setValue(filterOption.values[0]); + break; + case 'startDate': + this.filterForm.controls.fromDate.setValue(filterOption.values[0]); + break; + case 'endDate': + this.filterForm.controls.toDate.setValue(filterOption.values[0]); + break; + case 'status': + this.filterForm.controls.statusIn.setValue(scenarioExecutionStatusFromId(Number(filterOption.values[0])).name); + break; + } + }); + if (filters.filterOptions.length > 0) { + this.filterForm.markAsDirty(); + this.markFilterFormAsUnchanged(); + } + } + + private getFilterQueryParameter({ nameContains, fromDate, toDate, statusIn }: ScenarioExecutionFilter): { + [id: string]: any; + } { + return { + 'filter[scenarioName.contains]': nameContains ?? undefined, + 'filter[startDate.greaterThanOrEqual]': fromDate ? fromDate.toJSON() : undefined, + 'filter[endDate.lessThanOrEqual]': toDate ? toDate.toJSON() : undefined, + 'filter[status.in]': statusIn?.id ?? undefined, + }; + } + + private markFilterFormAsUnchanged(): void { + if (this.filterFormValueChanges) { + this.filterFormValueChanges.unsubscribe(); + } + + this.valueChanged = false; + this.filterFormValueChanges = this.filterForm.valueChanges.pipe(first()).subscribe({ next: () => (this.valueChanged = true) }); + } +} diff --git a/simulator-ui/src/main/webapp/app/scenario-result/scenario-result.component.html b/simulator-ui/src/main/webapp/app/scenario-result/scenario-result.component.html index 22bc6bec1..d8cfc195d 100644 --- a/simulator-ui/src/main/webapp/app/scenario-result/scenario-result.component.html +++ b/simulator-ui/src/main/webapp/app/scenario-result/scenario-result.component.html @@ -1,3 +1,18 @@
- +

+ Scenario Executions + +
+ +
+

+ +
+ +
+ +
diff --git a/simulator-ui/src/main/webapp/app/scenario-result/scenario-result.component.ts b/simulator-ui/src/main/webapp/app/scenario-result/scenario-result.component.ts index 76d25a405..0807d9862 100644 --- a/simulator-ui/src/main/webapp/app/scenario-result/scenario-result.component.ts +++ b/simulator-ui/src/main/webapp/app/scenario-result/scenario-result.component.ts @@ -1,12 +1,17 @@ -import { Component } from '@angular/core'; +import { Component, ViewChild } from '@angular/core'; import { ScenarioExecutionComponent } from 'app/entities/scenario-execution/list/scenario-execution.component'; import SharedModule from 'app/shared/shared.module'; +import ScenarioExecutionFilterComponent from './scenario-execution-filter.component'; + @Component({ standalone: true, selector: 'app-scenario-result', templateUrl: './scenario-result.component.html', - imports: [SharedModule, ScenarioExecutionComponent], + imports: [SharedModule, ScenarioExecutionComponent, ScenarioExecutionFilterComponent], }) -export default class ScenarioResultComponent {} +export default class ScenarioResultComponent { + @ViewChild(ScenarioExecutionComponent) + scenarioExecutionComponent: ScenarioExecutionComponent | null = null; +} diff --git a/simulator-ui/src/main/webapp/app/scenario/detail/scenario-detail.component.html b/simulator-ui/src/main/webapp/app/scenario/detail/scenario-detail.component.html index 8354c9537..3f8387425 100644 --- a/simulator-ui/src/main/webapp/app/scenario/detail/scenario-detail.component.html +++ b/simulator-ui/src/main/webapp/app/scenario/detail/scenario-detail.component.html @@ -50,7 +50,7 @@

+

diff --git a/simulator-ui/src/main/webapp/app/scenario/list/scenario.component.html b/simulator-ui/src/main/webapp/app/scenario/list/scenario.component.html index ab2c8146d..88ad4829e 100644 --- a/simulator-ui/src/main/webapp/app/scenario/list/scenario.component.html +++ b/simulator-ui/src/main/webapp/app/scenario/list/scenario.component.html @@ -50,7 +50,7 @@

Launch