From cc3d46ed4fa609160d7658a5e2ebc0ffcd42f1ce Mon Sep 17 00:00:00 2001 From: Goran Kokinovski Date: Fri, 26 May 2023 13:46:47 +0200 Subject: [PATCH] frontend: Implement executions list enhacements Executions list highlighting is in sync with duration chart focus on event (execution). Change label text on filter by period. Added caret next to text from time period filter. Implement synchronization between time period filter, duration zoom, grid filters and grid sort criteria. Whenever change occurs it is reflected on the other components/charts and everything is sharable through URL. Signed-off-by: Goran Kokinovski --- .../executions/data-job-executions.spec.js | 33 +- .../filters-sort-manager.spec.ts | 3 - .../filters-manager/filters-sort-manager.ts | 4 + .../data-job-execution-status.component.ts | 2 +- .../execution-default.comparator.spec.ts | 136 +++++ .../default/execution-default.comparator.ts | 44 ++ .../comparators/default/index.ts | 6 + .../execution-duration.comparator.spec.ts | 91 ++++ .../duration/execution-duration.comparator.ts | 29 + .../comparators/duration/index.ts | 6 + .../execution-duration-comparator.spec.ts | 79 --- .../execution-duration-comparator.ts | 25 - .../comparators/index.ts | 7 + .../criteria/index.ts | 8 + .../status/executions-status.criteria.spec.ts | 155 ++++++ .../status/executions-status.criteria.ts | 53 ++ .../criteria/status/index.ts | 6 + .../string/executions-string.criteria.spec.ts | 143 +++++ .../string/executions-string.criteria.ts | 48 ++ .../criteria/string/index.ts | 6 + .../type/executions-type.criteria.spec.ts | 156 ++++++ .../criteria/type/executions-type.criteria.ts | 53 ++ .../criteria/type/index.ts | 6 + .../data-job-executions-grid.component.html | 19 +- .../data-job-executions-grid.component.scss | 27 + .../data-job-executions-grid.component.ts | 250 ++++++++- .../data-job-executions-grid/index.ts | 2 +- .../executions-time-period.criteria.spec.ts | 146 +++++ .../executions-time-period.criteria.ts | 43 ++ .../criteria/time-period/index.ts | 6 + .../data-job-executions-page.component.html | 28 +- .../data-job-executions-page.component.ts | 151 ++++-- .../execution-duration-chart.component.ts | 325 ++++++++---- .../model/executions-filters.model.ts | 8 +- .../time-period-filter.component.html | 25 +- .../time-period-filter.component.scss | 3 + .../time-period-filter.component.ts | 501 +++++++++++------- .../shared/pipes/parse-next-run.pipe.spec.ts | 2 +- .../frontend/data-pipelines/gui/version.txt | 2 +- .../common/criteria/compound/and.criteria.ts | 40 ++ .../src/lib/common/criteria/compound/index.ts | 7 + .../common/criteria/compound/or.criteria.ts | 40 ++ .../shared/src/lib/common/criteria/index.ts | 7 + .../lib/common/criteria/primitive/index.ts | 6 + .../criteria/primitive/primitive.criteria.ts | 35 ++ .../src/lib/common/criteria/public-api.ts | 7 + .../projects/shared/src/lib/common/index.ts | 1 + .../common/interfaces/comparator.interface.ts | 14 + .../common/interfaces/criteria.interface.ts | 16 + .../shared/src/lib/common/interfaces/index.ts | 2 + .../shared/src/lib/common/public-api.ts | 1 + 51 files changed, 2323 insertions(+), 490 deletions(-) create mode 100644 projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/execution-default.comparator.spec.ts create mode 100644 projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/execution-default.comparator.ts create mode 100644 projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/index.ts create mode 100644 projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/execution-duration.comparator.spec.ts create mode 100644 projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/execution-duration.comparator.ts create mode 100644 projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/index.ts delete mode 100644 projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/execution-duration-comparator.spec.ts delete mode 100644 projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/execution-duration-comparator.ts create mode 100644 projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/index.ts create mode 100644 projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/index.ts create mode 100644 projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/executions-status.criteria.spec.ts create mode 100644 projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/executions-status.criteria.ts create mode 100644 projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/index.ts create mode 100644 projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/executions-string.criteria.spec.ts create mode 100644 projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/executions-string.criteria.ts create mode 100644 projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/index.ts create mode 100644 projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/executions-type.criteria.spec.ts create mode 100644 projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/executions-type.criteria.ts create mode 100644 projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/index.ts create mode 100644 projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/executions-time-period.criteria.spec.ts create mode 100644 projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/executions-time-period.criteria.ts create mode 100644 projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/index.ts create mode 100644 projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/compound/and.criteria.ts create mode 100644 projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/compound/index.ts create mode 100644 projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/compound/or.criteria.ts create mode 100644 projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/index.ts create mode 100644 projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/primitive/index.ts create mode 100644 projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/primitive/primitive.criteria.ts create mode 100644 projects/frontend/shared-components/gui/projects/shared/src/lib/common/criteria/public-api.ts create mode 100644 projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/comparator.interface.ts create mode 100644 projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/criteria.interface.ts diff --git a/projects/frontend/data-pipelines/gui/e2e/integration/manage/data-jobs/executions/data-job-executions.spec.js b/projects/frontend/data-pipelines/gui/e2e/integration/manage/data-jobs/executions/data-job-executions.spec.js index f55ee1b414..9a58124f4f 100644 --- a/projects/frontend/data-pipelines/gui/e2e/integration/manage/data-jobs/executions/data-job-executions.spec.js +++ b/projects/frontend/data-pipelines/gui/e2e/integration/manage/data-jobs/executions/data-job-executions.spec.js @@ -703,7 +703,7 @@ describe( pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, queryParams: { sort: '{"startTime":-1}', - filter: `{"startTimeFormatted":"${filterValue}"}` + filter: `{"startTime":"${filterValue}"}` } }); }); @@ -803,7 +803,7 @@ describe( pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, queryParams: { sort: '{"startTime":-1}', - filter: `{"endTimeFormatted":"${filterValue}"}` + filter: `{"endTime":"${filterValue}"}` } }); }); @@ -1799,7 +1799,8 @@ describe( }); }); - // !!! important tests order is very important, because generated url from 1st test is used in the second + // !!! important tests order is very important, because generated url from 1st test is used in the second test + // the second test cannot be run without previously running the first test describe('DataGrid Filters and Sort to URL', () => { // value is assigned at the end of the 1st test and used in the 2nd test let navigationUrlWithFiltersAndSort = ''; @@ -1845,15 +1846,16 @@ describe( } }); - // open type filter and select manual execution trigger and close + // open type filter and select manual execution then verify, then select scheduled trigger and close dataJobExecutionsPage.openTypeFilter(); dataJobExecutionsPage.filterByType('manual'); dataJobExecutionsPage .getDataGridExecTypeContainers('scheduled') .should('have.length', 0); + dataJobExecutionsPage.filterByType('scheduled'); dataJobExecutionsPage.closeFilter(); - // verify current URL has appended filters execution status: user_error, trigger type: manual, and sort by startTime descending + // verify current URL has appended filters execution status: user_error, trigger type: manual and scheduled, and sort by startTime descending dataJobExecutionsPage .getCurrentUrlNormalized({ includePathSegment: true, @@ -1864,7 +1866,7 @@ describe( pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, queryParams: { sort: '{"startTime":-1}', - filter: '{"status":"user_error","type":"manual"}' + filter: '{"status":"user_error","type":"manual,scheduled"}' } }); @@ -1890,7 +1892,7 @@ describe( return foundCells.length; }) .should('gt', 0); - // verify current URL has appended filters execution status: user_error, trigger type: manual, id: long_lived_job_name and sort by id ascending + // verify current URL has appended filters execution status: user_error, trigger type: manual and scheduled, id: long_lived_job_name and sort by id ascending dataJobExecutionsPage .getCurrentUrlNormalized({ includePathSegment: true, @@ -1901,7 +1903,7 @@ describe( pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, queryParams: { sort: '{"id":1}', - filter: `{"status":"user_error","type":"manual","id":"${longLivedFailingJobFixture.job_name}"}` + filter: `{"status":"user_error","type":"manual,scheduled","id":"${longLivedFailingJobFixture.job_name}"}` } }); @@ -1930,7 +1932,7 @@ describe( }) .should('gt', 0); - // verify current URL has appended filters execution status: user_error, trigger type: manual, id: long_lived_job_name, startTimeFormatted: ${filterValue} and sort by id ascending + // verify current URL has appended filters execution status: user_error, trigger type: manual and scheduled, id: long_lived_job_name, startTime: ${filterValue} and sort by id ascending dataJobExecutionsPage .getCurrentUrlNormalized({ includePathSegment: true, @@ -1941,7 +1943,7 @@ describe( pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, queryParams: { sort: '{"id":1}', - filter: `{"status":"user_error","type":"manual","id":"${longLivedFailingJobFixture.job_name}","startTimeFormatted":"${filterValue}"}` + filter: `{"status":"user_error","type":"manual,scheduled","id":"${longLivedFailingJobFixture.job_name}","startTime":"${filterValue}"}` } }); }); @@ -1972,7 +1974,7 @@ describe( }) .should('gt', 0); - // verify current URL has appended filters execution status: user_error, trigger type: manual, id: long_lived_job_name, startTimeFormatted: ${filterValue}, endTimeFormatted: ${filterValue} and sort by id ascending + // verify current URL has appended filters execution status: user_error, trigger type: manual and scheduled, id: long_lived_job_name, startTime: ${filterValue}, endTime: ${filterValue} and sort by id ascending dataJobExecutionsPage .getCurrentUrlNormalized({ includePathSegment: true, @@ -1983,7 +1985,7 @@ describe( pathSegment: `/manage/data-jobs/${longLivedFailingJobFixture.team}/${longLivedFailingJobFixture.job_name}/executions`, queryParams: { sort: '{"id":1}', - filter: `{"status":"user_error","type":"manual","id":"${longLivedFailingJobFixture.job_name}","startTimeFormatted":"${filterValue}","endTimeFormatted":"${filterValue}"}` + filter: `{"status":"user_error","type":"manual,scheduled","id":"${longLivedFailingJobFixture.job_name}","startTime":"${filterValue}","endTime":"${filterValue}"}` } }); }); @@ -2037,11 +2039,14 @@ describe( .getDataGridExecTypeFilterCheckboxesStatuses() .should('deep.equal', [ ['manual', true], - ['scheduled', false] + ['scheduled', true] ]); + dataJobExecutionsPage + .getDataGridExecTypeContainers('manual') + .should('have.length.gte', 0); dataJobExecutionsPage .getDataGridExecTypeContainers('scheduled') - .should('have.length', 0); + .should('have.length.gte', 0); dataJobExecutionsPage.closeFilter(); // verify sort by id ascending diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/filters-manager/filters-sort-manager.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/filters-manager/filters-sort-manager.spec.ts index bae7486d83..5b8f50b679 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/filters-manager/filters-sort-manager.spec.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/filters-manager/filters-sort-manager.spec.ts @@ -1495,7 +1495,6 @@ describe('FiltersSortManager', () => { // Given const filterValue1 = 'f1v,f11v'; const filterValue3 = 'f3v'; - const filterValue4 = 'f4v,f44v,f444v'; const sortValue1 = 's1v'; const sortValue2 = '-1'; const sortValue3 = 's3v'; @@ -1744,8 +1743,6 @@ describe('FiltersSortManager', () => { // Given const filterValue1 = 'f1v,f11v'; const filterValue3 = 'f3v'; - const filterValue4 = 'f4v,f44v,f444v'; - const sortValue1 = 's1v'; const sortValue2 = '-1'; const sortValue3 = 's3v'; const sortValue4 = 's4v'; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/filters-manager/filters-sort-manager.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/filters-manager/filters-sort-manager.ts index 8e03097df1..864a08c5ca 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/filters-manager/filters-sort-manager.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/commons/filters-manager/filters-sort-manager.ts @@ -344,6 +344,8 @@ export class FiltersSortManager< // debouncing for update URL, to avoid multiple updates when there are multiple serial near close update events this._updateTimeoutRef = setTimeout(() => { this._doUpdateBrowserUrl(strategy); + + this._updateTimeoutRef = null; }, this._debouncingTime); } @@ -355,6 +357,8 @@ export class FiltersSortManager< cancelScheduledBrowserUrlUpdate(): void { if (CollectionsUtil.isNumber(this._updateTimeoutRef)) { clearTimeout(this._updateTimeoutRef); + + this._updateTimeoutRef = null; } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-status/data-job-execution-status.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-status/data-job-execution-status.component.ts index c997a6d4c9..1c45ab0926 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-status/data-job-execution-status.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-execution-status/data-job-execution-status.component.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, HostListener, Input } from '@angular/core'; +import { Component, Input } from '@angular/core'; import { DataJobExecutionStatus } from '../../../../../model'; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/execution-default.comparator.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/execution-default.comparator.spec.ts new file mode 100644 index 0000000000..2ea83562ef --- /dev/null +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/execution-default.comparator.spec.ts @@ -0,0 +1,136 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DatePipe } from '@angular/common'; + +import { TestBed } from '@angular/core/testing'; + +import { DATA_PIPELINES_DATE_TIME_FORMAT } from '../../../../../../../model'; + +import { GridDataJobExecution } from '../../../model'; + +import { ExecutionDefaultComparator } from './execution-default.comparator'; + +describe('ExecutionDefaultComparator', () => { + let datePipe: DatePipe; + let dataJobExecutions: GridDataJobExecution[]; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DatePipe] + }); + + datePipe = TestBed.inject(DatePipe); + + const aStartTime = new Date(); + const aEndTime = new Date(aStartTime.getTime() + 100); + const bStartTime = new Date(); + const bEndTime = new Date(bStartTime.getTime() + 110); + + dataJobExecutions = [ + { + id: 'aJob', + startTimeFormatted: datePipe.transform(aStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), + startTime: aStartTime.toISOString(), + endTimeFormatted: datePipe.transform(aEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), + endTime: aEndTime.toISOString(), + duration: '100', + jobVersion: 'version-10' + }, + { + id: 'bJob', + startTimeFormatted: datePipe.transform(bStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), + startTime: bStartTime.toISOString(), + endTimeFormatted: datePipe.transform(bEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), + endTime: bEndTime.toISOString(), + duration: '110', + jobVersion: 'version-11' + } + ]; + }); + + describe('Properties::', () => { + describe('|direction|', () => { + it('should verify value', () => { + // Given + const instance = new ExecutionDefaultComparator('jobVersion', 'ASC'); + + // Then + expect(instance.property).toEqual('jobVersion'); + expect(instance.direction).toEqual('ASC'); + }); + }); + }); + + describe('Methods::', () => { + describe('|compare|', () => { + it('should verify will return -1 because of ascending sort', () => { + // Given + const instance = new ExecutionDefaultComparator('jobVersion', 'ASC'); + + // When + const res = instance.compare(dataJobExecutions[0], dataJobExecutions[1]); + + // Then + expect(res).toEqual(-1); + }); + + it('should verify will return 1 because of ascending sort', () => { + // Given + const instance = new ExecutionDefaultComparator('endTime', 'ASC'); + + // When + const res = instance.compare(dataJobExecutions[1], dataJobExecutions[0]); + + // Then + expect(res).toEqual(1); + }); + + it('should verify will return 1 because of descending sort', () => { + // Given + const instance = new ExecutionDefaultComparator('jobVersion', 'DESC'); + + // When + const res = instance.compare(dataJobExecutions[0], dataJobExecutions[1]); + + // Then + expect(res).toEqual(1); + }); + + it('should verify will return -1 because of descending sort', () => { + // Given + const instance = new ExecutionDefaultComparator('endTime', 'DESC'); + + // When + const res = instance.compare(dataJobExecutions[1], dataJobExecutions[0]); + + // Then + expect(res).toEqual(-1); + }); + + it('should verify will return 0 because of ascending sort', () => { + // Given + const instance = new ExecutionDefaultComparator('startTime', 'ASC'); + + // When + const res = instance.compare(dataJobExecutions[0], dataJobExecutions[1]); + + // Then + expect(res).toEqual(-0); + }); + + it('should verify will return 0 because of descending sort', () => { + // Given + const instance = new ExecutionDefaultComparator('startTime', 'DESC'); + + // When + const res = instance.compare(dataJobExecutions[0], dataJobExecutions[1]); + + // Then + expect(res).toEqual(0); + }); + }); + }); +}); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/execution-default.comparator.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/execution-default.comparator.ts new file mode 100644 index 0000000000..11123cbe07 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/execution-default.comparator.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { get } from 'lodash'; + +import { Comparator } from '@versatiledatakit/shared'; + +import { GridDataJobExecution } from '../../../model/data-job-execution'; + +/** + * ** Execution default comparator. + */ +export class ExecutionDefaultComparator implements Comparator { + /** + * ** Property path to value from GridDataJobExecution object. + */ + public readonly property: keyof GridDataJobExecution; + + /** + * ** Sort direction. + */ + public readonly direction: 'ASC' | 'DESC'; + + /** + * ** Constructor. + */ + constructor(property: keyof GridDataJobExecution, direction: 'ASC' | 'DESC') { + this.property = property; + this.direction = direction; + } + + /** + * @inheritDoc + */ + compare(exec1: GridDataJobExecution, exec2: GridDataJobExecution) { + const value1 = get(exec1, this.property); + const value2 = get(exec2, this.property); + const directionModifier = this.direction === 'DESC' ? 1 : -1; + + return (value1 > value2 ? -1 : value2 > value1 ? 1 : 0) * directionModifier; + } +} diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/index.ts new file mode 100644 index 0000000000..4cfd51c330 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/default/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright 2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './execution-default.comparator'; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/execution-duration.comparator.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/execution-duration.comparator.spec.ts new file mode 100644 index 0000000000..5432d68d7f --- /dev/null +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/execution-duration.comparator.spec.ts @@ -0,0 +1,91 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DatePipe } from '@angular/common'; + +import { TestBed } from '@angular/core/testing'; + +import { DATA_PIPELINES_DATE_TIME_FORMAT } from '../../../../../../../model'; + +import { GridDataJobExecution } from '../../../model'; + +import { ExecutionDurationComparator } from './execution-duration.comparator'; + +describe('ExecutionDurationComparator', () => { + let datePipe: DatePipe; + let dataJobExecutions: GridDataJobExecution[]; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DatePipe] + }); + + datePipe = TestBed.inject(DatePipe); + + const aStartTime = new Date(); + const aEndTime = new Date(aStartTime.getTime() + 100); + const bStartTime = new Date(); + const bEndTime = new Date(bStartTime.getTime() + 110); + + dataJobExecutions = [ + { + id: 'aJob', + startTimeFormatted: datePipe.transform(aStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), + startTime: aStartTime.toISOString(), + endTimeFormatted: datePipe.transform(aEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), + endTime: aEndTime.toISOString(), + duration: '100', + jobVersion: '' + }, + { + id: 'bJob', + startTimeFormatted: datePipe.transform(bStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), + startTime: bStartTime.toISOString(), + endTimeFormatted: datePipe.transform(bEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), + endTime: bEndTime.toISOString(), + duration: '110', + jobVersion: '' + } + ]; + }); + + describe('Properties::', () => { + describe('|direction|', () => { + it('should verify value', () => { + // Given + const instance = new ExecutionDurationComparator('ASC'); + + // Then + expect(instance.direction).toEqual('ASC'); + }); + }); + }); + + describe('Methods::', () => { + describe('|compare|', () => { + it('should verify will return -10 because of ascending sort', () => { + // Given + const instance = new ExecutionDurationComparator('ASC'); + + // When + const res = instance.compare(dataJobExecutions[0], dataJobExecutions[1]); + + // Then + expect(res).toEqual(-10); + }); + + it('should verify will return 10 because of descending sort', () => { + // Given + const instance = new ExecutionDurationComparator('DESC'); + + // When + const res = instance.compare(dataJobExecutions[0], dataJobExecutions[1]); + + // Then + expect(res).toEqual(10); + }); + }); + }); +}); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/execution-duration.comparator.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/execution-duration.comparator.ts new file mode 100644 index 0000000000..deb6ba7506 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/execution-duration.comparator.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Comparator } from '@versatiledatakit/shared'; + +import { GridDataJobExecution } from '../../../model/data-job-execution'; + +export class ExecutionDurationComparator implements Comparator { + public readonly direction: 'ASC' | 'DESC'; + + /** + * ** Constructor. + */ + constructor(direction: 'ASC' | 'DESC') { + this.direction = direction; + } + + /** + * @inheritDoc + */ + compare(exec1: GridDataJobExecution, exec2: GridDataJobExecution): number { + const aDuration = Date.parse(exec1.endTime) - Date.parse(exec1.startTime); + const bDuration = Date.parse(exec2.endTime) - Date.parse(exec2.startTime); + + return this.direction === 'ASC' ? aDuration - bDuration : bDuration - aDuration; + } +} diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/index.ts new file mode 100644 index 0000000000..82b07e8df5 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/duration/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright 2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './execution-duration.comparator'; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/execution-duration-comparator.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/execution-duration-comparator.spec.ts deleted file mode 100644 index c6025e9913..0000000000 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/execution-duration-comparator.spec.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2021-2023 VMware, Inc. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { DatePipe } from '@angular/common'; - -import { TestBed } from '@angular/core/testing'; - -import { CollectionsUtil } from '@versatiledatakit/shared'; - -import { DATA_PIPELINES_DATE_TIME_FORMAT } from '../../../../../../model'; - -import { DataJobExecutionDurationComparator } from './execution-duration-comparator'; - -describe('DataJobExecutionDurationComparator', () => { - let property: string; - let datePipe: DatePipe; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [DatePipe] - }); - - datePipe = TestBed.inject(DatePipe); - - property = CollectionsUtil.generateRandomString(); - }); - - describe('Properties::', () => { - describe('|property|', () => { - it('should verify value', () => { - // Given - const instance = new DataJobExecutionDurationComparator(property); - - // Then - expect(instance.property).toEqual(property); - }); - }); - }); - - describe('Methods::', () => { - describe('|compare|', () => { - it('should verify will return -10', () => { - // Given - const aStartTime = new Date(); - const aEndTime = new Date(aStartTime.getTime() + 100); - const bStartTime = new Date(); - const bEndTime = new Date(bStartTime.getTime() + 110); - const instance = new DataJobExecutionDurationComparator(property); - - // When - const res = instance.compare( - { - id: 'aJob', - startTimeFormatted: datePipe.transform(aStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), - startTime: aStartTime.toISOString(), - endTimeFormatted: datePipe.transform(aEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), - endTime: aEndTime.toISOString(), - duration: '100', - jobVersion: '' - }, - { - id: 'bJob', - startTimeFormatted: datePipe.transform(bStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), - startTime: bStartTime.toISOString(), - endTimeFormatted: datePipe.transform(bEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), - endTime: bEndTime.toISOString(), - duration: '110', - jobVersion: '' - } - ); - - // Then - expect(res).toEqual(-10); - }); - }); - }); -}); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/execution-duration-comparator.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/execution-duration-comparator.ts deleted file mode 100644 index 489d32f7cf..0000000000 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/execution-duration-comparator.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2021-2023 VMware, Inc. - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ClrDatagridComparatorInterface } from '@clr/angular'; - -import { GridDataJobExecution } from '../../model/data-job-execution'; - -export class DataJobExecutionDurationComparator implements ClrDatagridComparatorInterface { - /** - * ** Constructor. - */ - constructor(public readonly property: string) {} - - /** - * @inheritDoc - */ - compare(a: GridDataJobExecution, b: GridDataJobExecution) { - const aDuration = Date.parse(a.endTime) - Date.parse(a.startTime); - const bDuration = Date.parse(b.endTime) - Date.parse(b.startTime); - - return aDuration - bDuration; - } -} diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/index.ts new file mode 100644 index 0000000000..a50710e837 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/comparators/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright 2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './default'; +export * from './duration'; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/index.ts new file mode 100644 index 0000000000..25235dc4f5 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright 2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './status'; +export * from './string'; +export * from './type'; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/executions-status.criteria.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/executions-status.criteria.spec.ts new file mode 100644 index 0000000000..3eccd74425 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/executions-status.criteria.spec.ts @@ -0,0 +1,155 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DatePipe } from '@angular/common'; + +import { TestBed } from '@angular/core/testing'; + +import { CallFake, CollectionsUtil } from '@versatiledatakit/shared'; + +import { DATA_PIPELINES_DATE_TIME_FORMAT, DataJobExecutionStatus } from '../../../../../../../model'; + +import { GridDataJobExecution } from '../../../model'; + +import { ExecutionsStatusCriteria } from './executions-status.criteria'; + +describe('ExecutionsStatusCriteria', () => { + let datePipe: DatePipe; + let dataJobExecutions: GridDataJobExecution[]; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DatePipe] + }); + + datePipe = TestBed.inject(DatePipe); + + const aStartTime = new Date(); + const aEndTime = new Date(aStartTime.getTime() + 100); + const bStartTime = new Date(); + const bEndTime = new Date(bStartTime.getTime() + 110); + const cStartTime = new Date(); + const cEndTime = new Date(cStartTime.getTime() + 120); + + dataJobExecutions = [ + { + id: 'aJob', + startTimeFormatted: datePipe.transform(aStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), + startTime: aStartTime.toISOString(), + endTimeFormatted: datePipe.transform(aEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), + endTime: aEndTime.toISOString(), + duration: '100', + jobVersion: '', + status: DataJobExecutionStatus.SUCCEEDED + }, + { + id: 'bJob', + startTimeFormatted: datePipe.transform(bStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), + startTime: bStartTime.toISOString(), + endTimeFormatted: datePipe.transform(bEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), + endTime: bEndTime.toISOString(), + duration: '110', + jobVersion: '', + status: DataJobExecutionStatus.RUNNING + }, + { + id: 'cJob', + startTimeFormatted: datePipe.transform(cStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), + startTime: cStartTime.toISOString(), + endTimeFormatted: datePipe.transform(cEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), + endTime: cEndTime.toISOString(), + duration: '120', + jobVersion: '', + status: DataJobExecutionStatus.PLATFORM_ERROR + } + ]; + }); + + describe('Methods::', () => { + describe('|meetCriteria|', () => { + it('should verify will return Array with aJob and bJob', () => { + // Given + const instance = new ExecutionsStatusCriteria('succeeded,running'); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual([dataJobExecutions[0], dataJobExecutions[1]]); + }); + + it('should verify will return Array with cJob', () => { + // Given + const instance = new ExecutionsStatusCriteria('platform_error'); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual([dataJobExecutions[2]]); + }); + + it('should verify will return empty Array', () => { + // Given + const instance = new ExecutionsStatusCriteria('user_error'); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual([]); + }); + + it('should verify will return Array with all Jobs when serialized status criteria is empty string', () => { + // Given + const instance = new ExecutionsStatusCriteria(''); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual(dataJobExecutions); + }); + + it('should verify will return Array with all Jobs when serialized status criteria is Nil', () => { + // Given + const instance = new ExecutionsStatusCriteria(null); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual(dataJobExecutions); + }); + + it('should verify will return empty Array when Executions are Nil', () => { + // Given + const instance = new ExecutionsStatusCriteria('running'); + + // When + const res = instance.meetCriteria(null); + + // Then + expect(res).toEqual([]); + }); + + it('should verify will return Array with all Jobs when serialized status deserialization fails', () => { + // Given + spyOn(CollectionsUtil, 'isStringWithContent').and.throwError(new Error('String validation fails')); + const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); + const instance = new ExecutionsStatusCriteria('running'); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual(dataJobExecutions); + expect(consoleErrorSpy).toHaveBeenCalledWith( + `ExecutionsStatusCriteria: failed to deserialize Data Job Execution Statuses.` + ); + }); + }); + }); +}); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/executions-status.criteria.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/executions-status.criteria.ts new file mode 100644 index 0000000000..60a57686b5 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/executions-status.criteria.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CollectionsUtil, Criteria } from '@versatiledatakit/shared'; + +import { DataJobExecutionStatus } from '../../../../../../../model'; + +import { GridDataJobExecution } from '../../../model'; + +/** + * ** Executions Status filter criteria. + */ +export class ExecutionsStatusCriteria implements Criteria { + private readonly _dataJobExecutionStatuses: DataJobExecutionStatus[]; + + /** + * ** Constructor. + */ + constructor(dataJobExecutionStatusesSerialized: string) { + this._dataJobExecutionStatuses = ExecutionsStatusCriteria._deserializeExecutionStatuses(dataJobExecutionStatusesSerialized); + } + + /** + * @inheritDoc + */ + meetCriteria(executions: GridDataJobExecution[]): GridDataJobExecution[] { + return [...(executions ?? [])].filter((execution) => { + const status = execution.status; + + if (this._dataJobExecutionStatuses.length === 0) { + return true; + } + + return this._dataJobExecutionStatuses.includes(status); + }); + } + + private static _deserializeExecutionStatuses(dataJobExecutionStatusesSerialized: string): DataJobExecutionStatus[] { + try { + if (!CollectionsUtil.isStringWithContent(dataJobExecutionStatusesSerialized)) { + return []; + } + + return dataJobExecutionStatusesSerialized.toUpperCase().split(',') as DataJobExecutionStatus[]; + } catch (e) { + console.error(`ExecutionsStatusCriteria: failed to deserialize Data Job Execution Statuses.`); + + return []; + } + } +} diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/index.ts new file mode 100644 index 0000000000..bc59c2e4fb --- /dev/null +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/status/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright 2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './executions-status.criteria'; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/executions-string.criteria.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/executions-string.criteria.spec.ts new file mode 100644 index 0000000000..13daae4d28 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/executions-string.criteria.spec.ts @@ -0,0 +1,143 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DatePipe } from '@angular/common'; + +import { TestBed } from '@angular/core/testing'; + +import { DATA_PIPELINES_DATE_TIME_FORMAT, DataJobExecutionStatus, DataJobExecutionType } from '../../../../../../../model'; + +import { GridDataJobExecution } from '../../../model'; + +import { ExecutionsStringCriteria } from './executions-string.criteria'; + +describe('ExecutionsStringCriteria', () => { + let datePipe: DatePipe; + let dataJobExecutions: GridDataJobExecution[]; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DatePipe] + }); + + datePipe = TestBed.inject(DatePipe); + + const aStartTime = new Date(); + const aEndTime = new Date(aStartTime.getTime() + 100); + const bStartTime = new Date(); + const bEndTime = new Date(bStartTime.getTime() + 110); + const cStartTime = new Date(); + const cEndTime = new Date(cStartTime.getTime() + 120); + + dataJobExecutions = [ + { + id: 'aJob', + startTimeFormatted: datePipe.transform(aStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), + startTime: aStartTime.toISOString(), + endTimeFormatted: datePipe.transform(aEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), + endTime: aEndTime.toISOString(), + duration: '100', + jobVersion: 'aJob-10', + status: DataJobExecutionStatus.SUCCEEDED, + type: DataJobExecutionType.SCHEDULED, + opId: null + }, + { + id: 'bJob', + startTimeFormatted: datePipe.transform(bStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), + startTime: bStartTime.toISOString(), + endTimeFormatted: datePipe.transform(bEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), + endTime: bEndTime.toISOString(), + duration: '110', + jobVersion: 'bJob-11', + status: DataJobExecutionStatus.RUNNING, + type: DataJobExecutionType.SCHEDULED, + opId: null + }, + { + id: 'cJob', + startTimeFormatted: datePipe.transform(cStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), + startTime: cStartTime.toISOString(), + endTimeFormatted: datePipe.transform(cEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), + endTime: cEndTime.toISOString(), + duration: '120', + jobVersion: 'cJob-12', + status: DataJobExecutionStatus.PLATFORM_ERROR, + type: DataJobExecutionType.MANUAL, + opId: 'cJob_opId' + } + ]; + }); + + describe('Methods::', () => { + describe('|meetCriteria|', () => { + it('should verify will return Array with aJob, bJob and cJob because match is partial and case insensitive', () => { + // Given + const instance = new ExecutionsStringCriteria('jobVersion', 'job'); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual(dataJobExecutions); + }); + + it('should verify will return Array with cJob', () => { + // Given + const instance = new ExecutionsStringCriteria('endTime', dataJobExecutions[2].endTime); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual([dataJobExecutions[2]]); + }); + + it('should verify will return empty Array', () => { + // Given + const instance = new ExecutionsStringCriteria('id', 'aJobExecuted'); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual([]); + }); + + it('should verify will return only cJob because other opId are Nil', () => { + // Given + const instance = new ExecutionsStringCriteria('opId', 'opid'); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual([dataJobExecutions[2]]); + }); + + it('should verify will return Array with all Jobs when search value (query) is Nil', () => { + // Given + const instance = new ExecutionsStringCriteria('id', null); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual(dataJobExecutions); + }); + + it('should verify will return empty Array when Executions are Nil', () => { + // Given + const instance = new ExecutionsStringCriteria('startTimeFormatted', dataJobExecutions[0].startTimeFormatted); + + // When + const res = instance.meetCriteria(null); + + // Then + expect(res).toEqual([]); + }); + }); + }); +}); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/executions-string.criteria.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/executions-string.criteria.ts new file mode 100644 index 0000000000..307077d48a --- /dev/null +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/executions-string.criteria.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { get } from 'lodash'; + +import { CollectionsUtil, Criteria } from '@versatiledatakit/shared'; + +import { GridDataJobExecution } from '../../../model'; + +/** + * ** Executions Generic string filter criteria. + */ +export class ExecutionsStringCriteria implements Criteria { + private readonly _property: keyof GridDataJobExecution; + private readonly _searchValue: GridDataJobExecution[Exclude]; + + /** + * ** Constructor. + */ + constructor( + property: keyof GridDataJobExecution, + searchValue: GridDataJobExecution[Exclude] + ) { + this._property = property; + this._searchValue = searchValue; + } + + /** + * @inheritDoc + */ + meetCriteria(executions: GridDataJobExecution[]): GridDataJobExecution[] { + return [...(executions ?? [])].filter((execution) => { + const value = get(execution, this._property); + + if (!CollectionsUtil.isString(this._searchValue)) { + return true; + } + + if (!CollectionsUtil.isString(value)) { + return false; + } + + return value.toLowerCase().includes(this._searchValue.toLowerCase()); + }); + } +} diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/index.ts new file mode 100644 index 0000000000..504585814d --- /dev/null +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/string/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright 2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './executions-string.criteria'; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/executions-type.criteria.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/executions-type.criteria.spec.ts new file mode 100644 index 0000000000..5ee85ccde7 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/executions-type.criteria.spec.ts @@ -0,0 +1,156 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DatePipe } from '@angular/common'; + +import { TestBed } from '@angular/core/testing'; + +import { CallFake, CollectionsUtil } from '@versatiledatakit/shared'; + +import { DATA_PIPELINES_DATE_TIME_FORMAT, DataJobExecutionStatus, DataJobExecutionType } from '../../../../../../../model'; + +import { GridDataJobExecution } from '../../../model'; + +import { ExecutionsTypeCriteria } from './executions-type.criteria'; + +describe('ExecutionsTypeCriteria', () => { + let datePipe: DatePipe; + let dataJobExecutions: GridDataJobExecution[]; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DatePipe] + }); + + datePipe = TestBed.inject(DatePipe); + + const aStartTime = new Date(); + const aEndTime = new Date(aStartTime.getTime() + 100); + const bStartTime = new Date(); + const bEndTime = new Date(bStartTime.getTime() + 110); + const cStartTime = new Date(); + const cEndTime = new Date(cStartTime.getTime() + 120); + + dataJobExecutions = [ + { + id: 'aJob', + startTimeFormatted: datePipe.transform(aStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), + startTime: aStartTime.toISOString(), + endTimeFormatted: datePipe.transform(aEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), + endTime: aEndTime.toISOString(), + duration: '100', + jobVersion: '', + status: DataJobExecutionStatus.SUCCEEDED, + type: DataJobExecutionType.SCHEDULED + }, + { + id: 'bJob', + startTimeFormatted: datePipe.transform(bStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), + startTime: bStartTime.toISOString(), + endTimeFormatted: datePipe.transform(bEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), + endTime: bEndTime.toISOString(), + duration: '110', + jobVersion: '', + status: DataJobExecutionStatus.RUNNING, + type: DataJobExecutionType.SCHEDULED + }, + { + id: 'cJob', + startTimeFormatted: datePipe.transform(cStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), + startTime: cStartTime.toISOString(), + endTimeFormatted: datePipe.transform(cEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), + endTime: cEndTime.toISOString(), + duration: '120', + jobVersion: '', + status: DataJobExecutionStatus.PLATFORM_ERROR, + type: DataJobExecutionType.MANUAL + } + ]; + }); + + describe('Methods::', () => { + describe('|meetCriteria|', () => { + it('should verify will return Array with aJob and bJob', () => { + // Given + const instance = new ExecutionsTypeCriteria('scheduled'); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual([dataJobExecutions[0], dataJobExecutions[1]]); + }); + + it('should verify will return Array with cJob', () => { + // Given + const instance = new ExecutionsTypeCriteria('manual'); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual([dataJobExecutions[2]]); + }); + + it('should verify will return empty Array', () => { + // Given + const instance = new ExecutionsTypeCriteria('unknown_type'); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual([]); + }); + + it('should verify will return Array with all Jobs when serialized status criteria is empty string', () => { + // Given + const instance = new ExecutionsTypeCriteria(''); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual(dataJobExecutions); + }); + + it('should verify will return Array with all Jobs when serialized status criteria is Nil', () => { + // Given + const instance = new ExecutionsTypeCriteria(null); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual(dataJobExecutions); + }); + + it('should verify will return empty Array when Executions are Nil', () => { + // Given + const instance = new ExecutionsTypeCriteria('scheduled'); + + // When + const res = instance.meetCriteria(null); + + // Then + expect(res).toEqual([]); + }); + + it('should verify will return Array with all Jobs when serialized status deserialization fails', () => { + // Given + spyOn(CollectionsUtil, 'isStringWithContent').and.throwError(new Error('String validation fails')); + const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); + const instance = new ExecutionsTypeCriteria('scheduled'); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual(dataJobExecutions); + expect(consoleErrorSpy).toHaveBeenCalledWith(`ExecutionsTypeCriteria: failed to deserialize Data Job Execution Types.`); + }); + }); + }); +}); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/executions-type.criteria.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/executions-type.criteria.ts new file mode 100644 index 0000000000..5fce168eaf --- /dev/null +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/executions-type.criteria.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CollectionsUtil, Criteria } from '@versatiledatakit/shared'; + +import { DataJobExecutionType } from '../../../../../../../model'; + +import { GridDataJobExecution } from '../../../model'; + +/** + * ** Executions Type filter criteria. + */ +export class ExecutionsTypeCriteria implements Criteria { + private readonly _dataJobExecutionTypes: DataJobExecutionType[]; + + /** + * ** Constructor. + */ + constructor(dataJobExecutionTypesSerialized: string) { + this._dataJobExecutionTypes = ExecutionsTypeCriteria._deserializeExecutionTypes(dataJobExecutionTypesSerialized); + } + + /** + * @inheritDoc + */ + meetCriteria(executions: GridDataJobExecution[]): GridDataJobExecution[] { + return [...(executions ?? [])].filter((execution) => { + const type = execution.type; + + if (this._dataJobExecutionTypes.length === 0) { + return true; + } + + return this._dataJobExecutionTypes.includes(type); + }); + } + + private static _deserializeExecutionTypes(dataJobExecutionTypesSerialized: string): DataJobExecutionType[] { + try { + if (!CollectionsUtil.isStringWithContent(dataJobExecutionTypesSerialized)) { + return []; + } + + return dataJobExecutionTypesSerialized.toUpperCase().split(',') as DataJobExecutionType[]; + } catch (e) { + console.error(`ExecutionsTypeCriteria: failed to deserialize Data Job Execution Types.`); + + return []; + } + } +} diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/index.ts new file mode 100644 index 0000000000..0f79487edc --- /dev/null +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/criteria/type/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright 2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './executions-type.criteria'; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/data-job-executions-grid.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/data-job-executions-grid.component.html index 4ad81c87b4..83fdb1a1f8 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/data-job-executions-grid.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/data-job-executions-grid.component.html @@ -5,6 +5,7 @@ Duration Start (UTC) End (UTC) @@ -87,8 +88,8 @@ > Executions per page> = [ +const GRID_SUPPORTED_EXECUTIONS_FILTER_KEY: Array = [ FILTER_STATUS_KEY, FILTER_TYPE_KEY, FILTER_DURATION_KEY, @@ -54,7 +67,7 @@ const GRID_SUPPORTED_EXECUTIONS_FILTER_KEY: Array> = [ +const GRID_SUPPORTED_EXECUTIONS_SORT_KEY: Array = [ SORT_STATUS_KEY, SORT_TYPE_KEY, SORT_DURATION_KEY, @@ -64,13 +77,29 @@ const GRID_SUPPORTED_EXECUTIONS_SORT_KEY: Array> SORT_VERSION_KEY ]; +type GridExecutionFilterCriteria = Exclude; +type GridExecutionsFilterPairs = ExecutionsFilterPairs; + +type GridExecutionSortCriteria = Exclude; +type GridExecutionsSortPairs = ExecutionsFilterPairs; + +type GridStateLocal = { + filter: GridExecutionsFilterPairs[]; + sort: ExecutionsSortPairs; +}; + +export interface GridCriteriaAndComparator { + filter: Criteria; + sort: Comparator; +} + @Component({ selector: 'lib-data-job-executions-grid', templateUrl: './data-job-executions-grid.component.html', styleUrls: ['./data-job-executions-grid.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) -export class DataJobExecutionsGridComponent implements OnInit, OnDestroy { +export class DataJobExecutionsGridComponent implements OnChanges, OnInit, OnDestroy { @Input() jobExecutions: GridDataJobExecution[]; @Input() loading = false; @@ -81,14 +110,38 @@ export class DataJobExecutionsGridComponent implements OnInit, OnDestroy { FiltersSortManager >; + /** + * ** If provided will try to highlight row where execution id will match. + */ + @Input() highlightedExecutionId: string; + + /** + * ** Event Emitter that emits events on every user action on grid filters or sort. + */ + @Output() gridCriteriaAndComparatorChanged: EventEmitter = new EventEmitter(); + @HostBinding('attr.data-cy') public readonly attributeDataCy = 'data-pipelines-data-job-executions'; - // Sorting - durationComparator = new DataJobExecutionDurationComparator(SORT_DURATION_KEY); - // End of sorting openDeploymentDetailsModal = false; jobDeploymentModalData: DataJobDeployment; + paginatedJobExecutions: GridDataJobExecution[] = []; + + paginationPageNumber: number; + paginationPageSize: number; + paginationTotalItems: number; + + isInitialCriteriasEmit = true; + + private _appliedGridState: GridStateLocal = { + filter: [], + sort: undefined + }; + private _previousAppliedGridState: GridStateLocal = { + filter: [], + sort: undefined + }; + private _filterMutationObserver: FilterSortMutationObserver< ExecutionsFilterCriteria, string, @@ -96,6 +149,12 @@ export class DataJobExecutionsGridComponent implements OnInit, OnDestroy { ClrDatagridSortOrder >; + /** + * ** Reference to scheduled timeout for emitting Grid Criteria and Comparator. + * @private + */ + private _gridCriteriaAndComparatorEmitterTimeoutRef: number; + /** * ** Constructor. */ @@ -123,19 +182,46 @@ export class DataJobExecutionsGridComponent implements OnInit, OnDestroy { return; } + let skipCriteriaAndComparatorEmitterDebouncing = false; + + if (this.isInitialCriteriasEmit) { + this.isInitialCriteriasEmit = false; + skipCriteriaAndComparatorEmitterDebouncing = true; + } + this._populateManagerFilters(state); this._populateManagerSort(state); + this._evaluateGridStateMutation(skipCriteriaAndComparatorEmitterDebouncing); + + this._paginateExecutions(state); // update Browser URL once only, for every Grid event this.filtersSortManager.updateBrowserUrl(); } + /** + * @inheritDoc + */ + ngOnChanges(changes: SimpleChanges): void { + if ( + changes['jobExecutions'] && + !CollectionsUtil.isEqual(changes['jobExecutions'].previousValue, changes['jobExecutions'].currentValue) + ) { + this.paginationTotalItems = this.jobExecutions.length; + this._paginateExecutions(null); + } + } + /** * @inheritDoc */ ngOnInit(): void { this._filterMutationObserver = (changes) => { - if (changes.some(([key]) => [...GRID_SUPPORTED_EXECUTIONS_FILTER_KEY, ...GRID_SUPPORTED_EXECUTIONS_SORT_KEY].includes(key))) { + if ( + changes.some(([key]: GridExecutionsSortPairs) => + [...GRID_SUPPORTED_EXECUTIONS_FILTER_KEY, ...GRID_SUPPORTED_EXECUTIONS_SORT_KEY].includes(key) + ) + ) { this.changeDetectorRef.markForCheck(); } }; @@ -148,6 +234,10 @@ export class DataJobExecutionsGridComponent implements OnInit, OnDestroy { * @inheritDoc */ ngOnDestroy(): void { + if (CollectionsUtil.isNumber(this._gridCriteriaAndComparatorEmitterTimeoutRef)) { + clearTimeout(this._gridCriteriaAndComparatorEmitterTimeoutRef); + } + this.filtersSortManager.deleteMutationObserver(this._filterMutationObserver); } @@ -157,15 +247,18 @@ export class DataJobExecutionsGridComponent implements OnInit, OnDestroy { * @private */ private _populateManagerFilters(state: ClrDatagridStateInterface): void { + // on every grid emitted event save currently applied filters for comparison + this._previousAppliedGridState.filter = [...this._appliedGridState.filter]; + // when grid has user applied filters if (CollectionsUtil.isArray(state.filters)) { if (state.filters.length > 0) { - const newFilterPairs: ExecutionsFilterPairs[] = state.filters.map( - (filter: ExecutionsGridFilter) => [filter.property, filter.value] as ExecutionsFilterPairs + const newFilterPairs: GridExecutionsFilterPairs[] = state.filters.map( + (filter: ExecutionsGridFilter) => [filter.property, filter.value] as GridExecutionsFilterPairs ); // remove known filters if they are already set in the manager but are missing from grid state - const filtersForDeletion: ExecutionsFilterPairs[] = GRID_SUPPORTED_EXECUTIONS_FILTER_KEY.filter( + const filtersForDeletion: GridExecutionsFilterPairs[] = GRID_SUPPORTED_EXECUTIONS_FILTER_KEY.filter( (supportedCriteria) => this.filtersSortManager.hasFilter(supportedCriteria) && newFilterPairs.findIndex(([criteria]) => supportedCriteria === criteria) === -1 @@ -175,8 +268,14 @@ export class DataJobExecutionsGridComponent implements OnInit, OnDestroy { this.filtersSortManager.bulkUpdate(newFilterPairs.map(([criteria, value]) => [criteria, value, 'filter'])); + // set new filters to applied grid filters state + this._appliedGridState.filter = [...newFilterPairs]; + return; } + } else { + // clear applied grid filters state + this._appliedGridState.filter = []; } // when grid doesn't have user applied filters but manager has from previous actions @@ -184,7 +283,7 @@ export class DataJobExecutionsGridComponent implements OnInit, OnDestroy { // remove known filters if they are already set in the manager const filtersForDeletion = GRID_SUPPORTED_EXECUTIONS_FILTER_KEY.filter((criteria) => this.filtersSortManager.hasFilter(criteria) - ).map((criteria) => [criteria, null] as ExecutionsFilterPairs); + ).map((criteria) => [criteria, null] as GridExecutionsFilterPairs); if (filtersForDeletion.length > 0) { this.filtersSortManager.bulkUpdate(filtersForDeletion.map(([criteria, value]) => [criteria, value, 'filter'])); @@ -197,13 +296,15 @@ export class DataJobExecutionsGridComponent implements OnInit, OnDestroy { * @private */ private _populateManagerSort(state: ClrDatagridStateInterface): void { + // on every grid emitted event save currently applied sort pair + this._previousAppliedGridState.sort = this._appliedGridState.sort; + // when grid has user applied sort if (CollectionsUtil.isDefined(state.sort)) { const property: ExecutionsSortCriteria = CollectionsUtil.isStringWithContent(state.sort.by) ? (state.sort.by as ExecutionsSortCriteria) : (state.sort.by as unknown as { property: ExecutionsSortCriteria })?.property; const direction = state.sort.reverse ? ClrDatagridSortOrder.DESC : ClrDatagridSortOrder.ASC; - const newSortPairs: ExecutionsSortPairs[] = [[property, direction]]; // always remove known previous stored sort criteria and direction @@ -219,7 +320,13 @@ export class DataJobExecutionsGridComponent implements OnInit, OnDestroy { this.filtersSortManager.bulkUpdate(newSortPairs.map(([criteria, value]) => [criteria, value, 'sort'])); + // set new sort to applied grid sort state + this._appliedGridState.sort = newSortPairs[0]; + return; + } else { + // clear applied grid sort state + this._appliedGridState.sort = undefined; } // when grid doesn't have user applied sort but manager has from previous actions @@ -234,4 +341,115 @@ export class DataJobExecutionsGridComponent implements OnInit, OnDestroy { } } } + + private _paginateExecutions(state: ClrDatagridStateInterface): void { + this.paginationPageNumber = state?.page?.current ?? 1; + this.paginationPageSize = state?.page?.size ?? 10; + + const pageSize = CollectionsUtil.isDefined(this.paginationPageSize) ? this.paginationPageSize : 10; + const pageNumber = CollectionsUtil.isDefined(this.paginationPageNumber) ? this.paginationPageNumber - 1 : 0; + const from = pageNumber * pageSize; + const to = (pageNumber + 1) * pageSize; + + this.paginatedJobExecutions = this.jobExecutions.slice(from, to); + } + + private _evaluateGridStateMutation(skipDebouncing = false): void { + if ( + this._previousAppliedGridState.filter.length !== this._appliedGridState.filter.length || + this._previousAppliedGridState.sort !== this._appliedGridState.sort + ) { + this._emitGridCriteriaAndComparator(skipDebouncing); + + return; + } + + if (this._previousAppliedGridState.filter.length === this._appliedGridState.filter.length) { + if (!CollectionsUtil.isEqual(this._previousAppliedGridState.filter, this._appliedGridState.filter)) { + this._emitGridCriteriaAndComparator(skipDebouncing); + + return; + } + } + + if (!CollectionsUtil.isEqual(this._previousAppliedGridState.sort, this._appliedGridState.sort)) { + this._emitGridCriteriaAndComparator(skipDebouncing); + + return; + } + } + + private _emitGridCriteriaAndComparator(skipDebouncing = false): void { + if (CollectionsUtil.isNumber(this._gridCriteriaAndComparatorEmitterTimeoutRef)) { + clearTimeout(this._gridCriteriaAndComparatorEmitterTimeoutRef); + + this._gridCriteriaAndComparatorEmitterTimeoutRef = null; + } + + if (skipDebouncing) { + this.gridCriteriaAndComparatorChanged.emit({ + filter: this._createFilterCriteria(), + sort: this._createSortComparator() + }); + + return; + } + + this._gridCriteriaAndComparatorEmitterTimeoutRef = setTimeout(() => { + this.gridCriteriaAndComparatorChanged.emit({ + filter: this._createFilterCriteria(), + sort: this._createSortComparator() + }); + + this._gridCriteriaAndComparatorEmitterTimeoutRef = null; + }, 200); + } + + private _createFilterCriteria(): Criteria { + const criteria: Criteria[] = []; + + for (const filterPair of this._appliedGridState.filter) { + if (filterPair[0] === 'status') { + criteria.push(new ExecutionsStatusCriteria(filterPair[1])); + + continue; + } + + if (filterPair[0] === 'type') { + criteria.push(new ExecutionsTypeCriteria(filterPair[1])); + + continue; + } + + if (filterPair[0] === 'startTime') { + criteria.push(new ExecutionsStringCriteria('startTimeFormatted', filterPair[1])); + + continue; + } + + if (filterPair[0] === 'endTime') { + criteria.push(new ExecutionsStringCriteria('endTimeFormatted', filterPair[1])); + + continue; + } + + criteria.push(new ExecutionsStringCriteria(filterPair[0], filterPair[1])); + } + + return criteria.length > 0 ? new AndCriteria(...criteria) : null; + } + + private _createSortComparator(): Comparator { + if (CollectionsUtil.isDefined(this._appliedGridState.sort)) { + const [sortCriteria, sortValue] = this._appliedGridState.sort; + + if (sortCriteria === 'duration') { + return new ExecutionDurationComparator(sortValue === ClrDatagridSortOrder.ASC ? 'ASC' : 'DESC'); + } + + return new ExecutionDefaultComparator(sortCriteria, sortValue === ClrDatagridSortOrder.ASC ? 'ASC' : 'DESC'); + } + + return null; + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/index.ts index bdc2c7ff67..1b806f219e 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/index.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-grid/index.ts @@ -4,4 +4,4 @@ */ export * from './data-job-executions-grid.component'; -export * from './comparators/execution-duration-comparator'; +export * from './comparators/duration/execution-duration.comparator'; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/executions-time-period.criteria.spec.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/executions-time-period.criteria.spec.ts new file mode 100644 index 0000000000..1397f02f83 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/executions-time-period.criteria.spec.ts @@ -0,0 +1,146 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { DatePipe } from '@angular/common'; + +import { TestBed } from '@angular/core/testing'; + +import { DATA_PIPELINES_DATE_TIME_FORMAT, DataJobExecutionStatus, DataJobExecutionType } from '../../../../../../../model'; + +import { GridDataJobExecution } from '../../../model'; + +import { ExecutionsTimePeriodCriteria } from './executions-time-period.criteria'; + +describe('ExecutionsTimePeriodCriteria', () => { + let datePipe: DatePipe; + let dataJobExecutions: GridDataJobExecution[]; + + let aStartTime: Date; + let bStartTime: Date; + let cStartTime: Date; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DatePipe] + }); + + datePipe = TestBed.inject(DatePipe); + + const currentTime = new Date(); + + aStartTime = new Date(currentTime.getTime() + 10); + const aEndTime = new Date(aStartTime.getTime() + 110); + bStartTime = new Date(currentTime.getTime() + 20); + const bEndTime = new Date(bStartTime.getTime() + 120); + cStartTime = new Date(currentTime.getTime() + 30); + const cEndTime = new Date(cStartTime.getTime() + 130); + + dataJobExecutions = [ + { + id: 'aJob', + startTimeFormatted: datePipe.transform(aStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), + startTime: aStartTime.toISOString(), + endTimeFormatted: datePipe.transform(aEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), + endTime: aEndTime.toISOString(), + duration: '100', + jobVersion: '', + status: DataJobExecutionStatus.SUCCEEDED, + type: DataJobExecutionType.SCHEDULED + }, + { + id: 'bJob', + startTimeFormatted: datePipe.transform(bStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), + startTime: bStartTime.toISOString(), + endTimeFormatted: datePipe.transform(bEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), + endTime: bEndTime.toISOString(), + duration: '110', + jobVersion: '', + status: DataJobExecutionStatus.RUNNING, + type: DataJobExecutionType.SCHEDULED + }, + { + id: 'cJob', + startTimeFormatted: datePipe.transform(cStartTime, DATA_PIPELINES_DATE_TIME_FORMAT), + startTime: cStartTime.toISOString(), + endTimeFormatted: datePipe.transform(cEndTime, DATA_PIPELINES_DATE_TIME_FORMAT), + endTime: cEndTime.toISOString(), + duration: '120', + jobVersion: '', + status: DataJobExecutionStatus.PLATFORM_ERROR, + type: DataJobExecutionType.MANUAL + } + ]; + }); + + describe('Methods::', () => { + describe('|meetCriteria|', () => { + it('should verify will return Array with aJob and bJob', () => { + // Given + const instance = new ExecutionsTimePeriodCriteria(aStartTime, bStartTime); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual([dataJobExecutions[0], dataJobExecutions[1]]); + }); + + it('should verify will return Array with cJob', () => { + // Given + const instance = new ExecutionsTimePeriodCriteria(cStartTime, cStartTime); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual([dataJobExecutions[2]]); + }); + + it('should verify will return empty Array when startTime is Nil', () => { + // Given + const instance = new ExecutionsTimePeriodCriteria(aStartTime, cStartTime); + + // When + const res = instance.meetCriteria(dataJobExecutions.map((ex) => ({ ...ex, startTime: null }))); + + // Then + expect(res).toEqual([]); + }); + + it('should verify will return Array with all Jobs when from time is Nil', () => { + // Given + const instance = new ExecutionsTimePeriodCriteria(null, cStartTime); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual(dataJobExecutions); + }); + + it('should verify will return Array with all Jobs when to time is Nil', () => { + // Given + const instance = new ExecutionsTimePeriodCriteria(aStartTime, null); + + // When + const res = instance.meetCriteria(dataJobExecutions); + + // Then + expect(res).toEqual(dataJobExecutions); + }); + + it('should verify will return empty Array when Executions are Nil', () => { + // Given + const instance = new ExecutionsTimePeriodCriteria(aStartTime, cStartTime); + + // When + const res = instance.meetCriteria(null); + + // Then + expect(res).toEqual([]); + }); + }); + }); +}); diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/executions-time-period.criteria.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/executions-time-period.criteria.ts new file mode 100644 index 0000000000..4f996bb2c9 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/executions-time-period.criteria.ts @@ -0,0 +1,43 @@ +/* + * Copyright 2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CollectionsUtil, Criteria } from '@versatiledatakit/shared'; + +import { GridDataJobExecution } from '../../../model'; + +/** + * ** Executions Time Period filter criteria. + */ +export class ExecutionsTimePeriodCriteria implements Criteria { + private readonly _fromDateTime: Date; + private readonly _toDateTime: Date; + + /** + * ** Constructor. + */ + constructor(fromDateTime: Date, toDateTime: Date) { + this._fromDateTime = fromDateTime; + this._toDateTime = toDateTime; + } + + /** + * @inheritDoc + */ + meetCriteria(executions: GridDataJobExecution[]): GridDataJobExecution[] { + return [...(executions ?? [])].filter((execution) => { + if (CollectionsUtil.isNil(this._fromDateTime) || CollectionsUtil.isNil(this._toDateTime)) { + return true; + } + + if (!CollectionsUtil.isString(execution.startTime)) { + return false; + } + + const startTime = new Date(execution.startTime).getTime(); + + return this._fromDateTime.getTime() <= startTime && startTime <= this._toDateTime.getTime(); + }); + } +} diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/index.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/index.ts new file mode 100644 index 0000000000..e529027b37 --- /dev/null +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/criteria/time-period/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright 2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './executions-time-period.criteria'; diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/data-job-executions-page.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/data-job-executions-page.component.html index 5e8f51150f..8a1f55b84d 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/data-job-executions-page.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/data-job-executions-page.component.html @@ -6,13 +6,22 @@
+
@@ -21,7 +30,7 @@ class="btn btn-icon btn-link refresh" data-cy="data-pipelines-job-executions-refresh-btn" aria-label="refresh" - (click)="fetchDataJobExecutions()" + (click)="refresh()" > @@ -33,14 +42,17 @@
@@ -48,9 +60,11 @@
diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/data-job-executions-page.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/data-job-executions-page.component.ts index 26bc825181..580cc31585 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/data-job-executions-page.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/data-job-executions-page/data-job-executions-page.component.ts @@ -16,6 +16,7 @@ import { CollectionsUtil, ComponentModel, ComponentService, + Criteria, ErrorHandlerService, ErrorRecord, NavigationService, @@ -34,11 +35,9 @@ import { DataJobUtil, ErrorUtil } from '../../../../../shared/utils'; import { FiltersSortManager } from '../../../../../commons'; import { - DataJobExecutionFilter, DataJobExecutionOrder, DataJobExecutions, DataPipelinesRouteData, - FILTER_REQ_PARAM, JOB_EXECUTIONS_DATA_KEY, JOB_NAME_REQ_PARAM, ORDER_REQ_PARAM, @@ -62,6 +61,15 @@ import { SUPPORTED_EXECUTIONS_SORT_CRITERIA } from '../model/executions-filters.model'; +import { GridCriteriaAndComparator } from '../data-job-executions-grid'; + +import { ExecutionsTimePeriodCriteria } from './criteria/time-period'; + +interface SelectedDateTimePeriod { + from: Date; + to: Date; +} + @Component({ selector: 'lib-data-job-executions-page', templateUrl: './data-job-executions-page.component.html', @@ -78,16 +86,43 @@ export class DataJobExecutionsPageComponent jobName: string; isJobEditable = false; - jobExecutions: GridDataJobExecution[]; + jobExecutions: GridDataJobExecution[] = []; + filteredJobExecutions: GridDataJobExecution[] = []; minJobExecutionTime: Date; loading = true; initialLoading = true; - dateTimeFilter: { fromTime: Date; toTime: Date } = { - fromTime: null, - toTime: null + /** + * ** Selected DateTime period in time period filter. + */ + selectedPeriod: SelectedDateTimePeriod = { + from: null, + to: null + }; + + /** + * ** Indicates whether time filter is chosen, period is selected. + */ + isPeriodSelected = false; + + /** + * ** Zoomed DateTime period, in duration chart. + */ + zoomedPeriod: SelectedDateTimePeriod = { + from: null, + to: null }; + /** + * ** Focused (highlighted) execution id in duration chart. + */ + highlightedExecutionId: string; + + /** + * ** Grid Criteria and Comparator from Executions Data Grid. + */ + gridCriteriaAndComparator: GridCriteriaAndComparator; + /** * ** Array of error code patterns that component should listen for in errors store. */ @@ -144,26 +179,45 @@ export class DataJobExecutionsPageComponent this.navigateBack({ '$.team': this.teamName }).then(); } - onTimeFilterChange(dateTimeFilter: { fromTime: Date; toTime: Date }): void { - this.dateTimeFilter = dateTimeFilter; + timeFilterChange(selectedPeriod: SelectedDateTimePeriod): void { + if (this.selectedPeriod.from === selectedPeriod.from && this.selectedPeriod.to === selectedPeriod.to) { + return; + } - const modelFilter: DataJobExecutionFilter = this.model.getComponentState().requestParams.get(FILTER_REQ_PARAM) ?? {}; + this.selectedPeriod = selectedPeriod; - if (CollectionsUtil.isNil(dateTimeFilter.fromTime) || CollectionsUtil.isNil(dateTimeFilter.toTime)) { - delete modelFilter.startTimeGte; - delete modelFilter.startTimeLte; + this.isPeriodSelected = this.selectedPeriod.from !== null && this.selectedPeriod.to !== null; - this.model.withRequestParam(FILTER_REQ_PARAM, { - ...modelFilter - } as DataJobExecutionFilter); - } else { - this.model.withRequestParam(FILTER_REQ_PARAM, { - ...modelFilter, - startTimeGte: dateTimeFilter.fromTime, - startTimeLte: dateTimeFilter.toTime - } as DataJobExecutionFilter); + this._filterExecutions(); + } + + /** + * ** Executed whenever focus on execution id in duration chart changes. + */ + durationChartExecutionIdFocusChange(executionId: string): void { + this.highlightedExecutionId = executionId; + } + + durationChartZoomPeriodChange(zoomedPeriod: SelectedDateTimePeriod): void { + if ( + this.selectedPeriod.from === zoomedPeriod.from && + this.selectedPeriod.to === zoomedPeriod.to && + this.zoomedPeriod.from === zoomedPeriod.from && + this.zoomedPeriod.to === zoomedPeriod.to + ) { + return; } + this.zoomedPeriod = zoomedPeriod; + } + + gridCriteriaAndComparatorChange($event: GridCriteriaAndComparator): void { + this.gridCriteriaAndComparator = $event; + + this._filterExecutions(); + } + + refresh(): void { this.fetchDataJobExecutions(); } @@ -312,24 +366,15 @@ export class DataJobExecutionsPageComponent .pipe(map(DataJobExecutionToGridDataJobExecution.convertToDataJobExecution(this.datePipe))) .subscribe({ next: (values) => { - this.jobExecutions = values.filter((ex) => { - if (CollectionsUtil.isNil(this.dateTimeFilter.fromTime) || CollectionsUtil.isNil(this.dateTimeFilter.toTime)) { - return true; - } - - if (!CollectionsUtil.isString(ex.startTime)) { - return false; - } - - const startTime = new Date(ex.startTime); + this.jobExecutions = values; - return startTime > this.dateTimeFilter.fromTime && startTime < this.dateTimeFilter.toTime; - }); + this._filterExecutions(); if (this.jobExecutions.length > 0) { - const newMinJobExecutionsTime = new Date( - this.jobExecutions.reduce((prev, curr) => (prev.startTime < curr.startTime ? prev : curr)).startTime - ); + const oldestExecutionStartTime = [...this.jobExecutions] + .sort((ex1, ex2) => (ex1.startTime < ex2.startTime ? 1 : -1)) + .pop().startTime; + const newMinJobExecutionsTime = new Date(oldestExecutionStartTime); if ( CollectionsUtil.isNil(this.minJobExecutionTime) || @@ -337,6 +382,8 @@ export class DataJobExecutionsPageComponent ) { this.minJobExecutionTime = newMinJobExecutionsTime; } + } else { + this.minJobExecutionTime = null; } }, error: (error: unknown) => { @@ -345,4 +392,36 @@ export class DataJobExecutionsPageComponent }) ); } + + private _filterExecutions(): void { + let timePeriodCriteria: Criteria; + let executionsFilteredAndSorted: GridDataJobExecution[]; + + if (CollectionsUtil.isDefined(this.selectedPeriod.from) && CollectionsUtil.isDefined(this.selectedPeriod.to)) { + timePeriodCriteria = new ExecutionsTimePeriodCriteria(this.selectedPeriod.from, this.selectedPeriod.to); + // execute filter by time period + executionsFilteredAndSorted = timePeriodCriteria.meetCriteria(this.jobExecutions); + } else { + executionsFilteredAndSorted = [...this.jobExecutions]; + } + + if (this.gridCriteriaAndComparator) { + if (this.gridCriteriaAndComparator.filter) { + executionsFilteredAndSorted = this.gridCriteriaAndComparator.filter.meetCriteria(executionsFilteredAndSorted); + } + + if (this.gridCriteriaAndComparator.sort) { + executionsFilteredAndSorted = executionsFilteredAndSorted.sort( + this.gridCriteriaAndComparator.sort.compare.bind(this.gridCriteriaAndComparator.sort) as ( + a: GridDataJobExecution, + b: GridDataJobExecution + ) => number + ); + } + } + + this.filteredJobExecutions = executionsFilteredAndSorted; + + this.minJobExecutionTime = new Date(this.minJobExecutionTime); + } } diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-duration-chart/execution-duration-chart.component.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-duration-chart/execution-duration-chart.component.ts index 72726627cf..47602a871d 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-duration-chart/execution-duration-chart.component.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/execution-duration-chart/execution-duration-chart.component.ts @@ -3,10 +3,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { DatePipe, formatDate } from '@angular/common'; -import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { DatePipe } from '@angular/common'; +import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; -import { Chart, ChartData, registerables, ScatterDataPoint, TimeUnit } from 'chart.js'; +import { ActiveElement, Chart, ChartData, registerables, ScatterDataPoint, TimeUnit } from 'chart.js'; import ChartDataLabels from 'chartjs-plugin-datalabels'; import zoomPlugin from 'chartjs-plugin-zoom'; import 'chartjs-adapter-date-fns'; @@ -19,14 +19,20 @@ import { DATA_PIPELINES_DATE_TIME_FORMAT, DataJobExecutionStatus } from '../../. import { DataJobExecutionToGridDataJobExecution, GridDataJobExecution } from '../model'; -type CustomChartData = ScatterDataPoint & { +type CustomChartData = Partial & { startTime: number; duration: number; - endTime: number; + endTime: string; status: DataJobExecutionStatus; opId: string; + id: string; }; +interface ZoomPeriod { + from: Date; + to: Date; +} + @Component({ selector: 'lib-execution-duration-chart', templateUrl: './execution-duration-chart.component.html', @@ -36,68 +42,54 @@ type CustomChartData = ScatterDataPoint & { export class ExecutionDurationChartComponent implements OnInit, OnChanges { @Input() jobExecutions: GridDataJobExecution[] = []; - chartZoomed = false; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - chart: Chart<'line', any, unknown>; - - constructor() { - Chart.register(...registerables, ChartDataLabels, zoomPlugin); - } + /** + * ** Flag that indicates if duration chart is zoomed or not. + */ + @Input() chartZoomed = false; - getChartLabels(): number[] { - return this.jobExecutions.map((execution) => DateUtil.normalizeToUTC(execution.startTime).getTime()); - } + /** + * ** Emits event whenever focus on execution changes. + * + * - Value could be either executionId or null. + */ + @Output() executionIdFocused = new EventEmitter(); - getData(min?: number, max?: number): CustomChartData[] { - const divider = this.getDurationUnit().divider; - const executions = CollectionsUtil.isDefined(min) - ? this.jobExecutions.filter((ex) => { - const startTime = DateUtil.normalizeToUTC(ex.startTime).getTime(); - - return startTime >= min && startTime <= max; - }) - : this.jobExecutions; - - return executions.map((execution) => { - return { - startTime: DateUtil.normalizeToUTC(execution.startTime).getTime(), - duration: Math.round((this.getJobDurationSeconds(execution) / divider) * 100) / 100, - endTime: execution.endTime ? new Date(execution.endTime) : undefined, - status: execution.status, - opId: execution.opId - } as unknown as CustomChartData; - }); - } + /** + * ** Event Emitter that emits events on every user zoom period change in duration chart or reset zoom. + */ + @Output() zoomPeriodChanged: EventEmitter = new EventEmitter(); - getMaxDurationSeconds(): number { - return this.jobExecutions - .map((execution) => this.getJobDurationSeconds(execution)) - .reduce((prev, current) => (prev > current ? prev : current)); - } + /** + * ** Reference to Duration chart instance. + */ + chart: Chart<'line', CustomChartData[], number>; - getJobDurationSeconds(execution: GridDataJobExecution) { - return ( - ((execution.endTime ? new Date(execution.endTime).getTime() : new Date(Date.now()).getTime()) - - new Date(execution.startTime).getTime()) / - 1000 - ); - } + /** + * ** Currently focussed execution id, it could be either string if there is focussed execution or null if nothing is focussed. + * @private + */ + private _focusedExecutionId: CustomChartData['id']; - getDurationUnit(): { name: string; divider: number } { - const maxDurationSeconds = this.getMaxDurationSeconds(); + /** + * ** Zoom selection reference with from and to values. + * @private + */ + private _zoomPeriod: ZoomPeriod = { + from: null, + to: null + }; - if (maxDurationSeconds > 60) { - return maxDurationSeconds > 3600 ? { name: 'hours', divider: 3600 } : { name: 'minutes', divider: 60 }; - } else { - return { name: 'seconds', divider: 1 }; - } + constructor(private readonly datePipe: DatePipe) { + Chart.register(...registerables, ChartDataLabels, zoomPlugin); } resetZoom() { - this._adjustTimeScaleUnit(null); - this.chartZoomed = false; - this.chart.resetZoom(); + this._zoomPeriod = { + from: null, + to: null + }; + + this.zoomPeriodChanged.next(this._zoomPeriod); } /** @@ -105,9 +97,7 @@ export class ExecutionDurationChartComponent implements OnInit, OnChanges { */ ngOnChanges(changes: SimpleChanges): void { if (!changes['jobExecutions'].firstChange) { - this.chart.data.labels = this.getChartLabels(); - this.chart.data.datasets[0].data = this.getData(); - this.chart.update(); + this._updateChart(); } } @@ -119,17 +109,21 @@ export class ExecutionDurationChartComponent implements OnInit, OnChanges { } private _initChart(): void { - const data: ChartData<'line'> = { - labels: this.getChartLabels(), + const chartData: CustomChartData[] = this._getChartData(); + const unit: TimeUnit = this._getTimeScaleUnit(chartData); + const [min, max] = this._getMinMaxExecutionTupleAdjusted(chartData, unit); + + const data: ChartData<'line', CustomChartData[], number> = { + labels: this._getChartLabels(), datasets: [ { - data: this.getData(), + data: chartData, fill: false, pointRadius: 3, pointBorderColor: (context) => - DataJobExecutionToGridDataJobExecution.resolveColor((context.raw as { status: string }).status), + DataJobExecutionToGridDataJobExecution.resolveColor((context.raw as { status: string })?.status), pointBackgroundColor: (context) => - DataJobExecutionToGridDataJobExecution.resolveColor((context.raw as { status: string }).status), + DataJobExecutionToGridDataJobExecution.resolveColor((context.raw as { status: string })?.status), pointBorderWidth: 3, parsing: { xAxisKey: 'startTime', @@ -139,22 +133,28 @@ export class ExecutionDurationChartComponent implements OnInit, OnChanges { ] }; - this.chart = new Chart<'line'>('durationChart', { + this.chart = new Chart<'line', CustomChartData[], number>('durationChart', { type: 'line', data, options: { + // callback listen for hover events in duration chart and process events + onHover: (event, activeElements) => { + this._emitFocussedExecutionId(activeElements); + }, showLine: false, scales: { x: { type: 'time', time: { - unit: this._getTimeScaleUnit(...this._getMinMaxExecutionTuple()) - } + unit + }, + min, + max }, y: { title: { display: true, - text: `Duration ${this.getDurationUnit().name}` + text: `Duration ${this._getDurationUnit().name}` } } }, @@ -167,8 +167,19 @@ export class ExecutionDurationChartComponent implements OnInit, OnChanges { }, mode: 'x', onZoomComplete: (context) => { - this._adjustTimeScaleUnit(context.chart); - this.chartZoomed = true; + const from = new Date(Math.floor(context.chart.scales['x'].min)); + const to = new Date(Math.ceil(context.chart.scales['x'].max)); + + if (this._zoomPeriod.from === from && this._zoomPeriod.to === to) { + return; + } + + this._zoomPeriod = { + from, + to + }; + + this.zoomPeriodChanged.next(this._zoomPeriod); } } }, @@ -181,16 +192,13 @@ export class ExecutionDurationChartComponent implements OnInit, OnChanges { tooltip: { callbacks: { label: (context) => { - const rawValues = context.raw as { - status: string; - endTime: Date; - }; + const rawValues = context.raw as CustomChartData; // eslint-disable-next-line @typescript-eslint/restrict-plus-operands return ( - `Duration: ${context.parsed.y}|${rawValues.status}` + + `Duration: ${context.parsed.y} | ${rawValues.status}` + (rawValues.endTime - ? `|End: ${formatDate(rawValues.endTime, DATA_PIPELINES_DATE_TIME_FORMAT, 'en-US', 'UTC')}` + ? ` | End: ${this.datePipe.transform(rawValues.endTime, DATA_PIPELINES_DATE_TIME_FORMAT, 'UTC')}` : '') ); } @@ -201,19 +209,14 @@ export class ExecutionDurationChartComponent implements OnInit, OnChanges { }); } - private _adjustTimeScaleUnit(chart: Chart): void { - let unit: TimeUnit; - let min: number = null; - let max: number = null; - - if (CollectionsUtil.isDefined(chart)) { - min = chart.scales['x'].min; - max = chart.scales['x'].max; + private _updateChart(): void { + const chartLabels: number[] = this._getChartLabels(); + const chartData: CustomChartData[] = this._getChartData(); + const unit: TimeUnit = this._getTimeScaleUnit(chartData); + const [min, max] = this._getMinMaxExecutionTupleAdjusted(chartData, unit); - unit = this._getTimeScaleUnit(min, max); - } else { - unit = this._getTimeScaleUnit(...this._getMinMaxExecutionTuple()); - } + this.chart.data.labels = chartLabels; + this.chart.data.datasets[0].data = chartData; this.chart.options.scales['x'] = { type: 'time', @@ -224,22 +227,33 @@ export class ExecutionDurationChartComponent implements OnInit, OnChanges { max }; - this.chart.data.datasets[0].data = this.getData(min, max); - this.chart.update(); } - private _getMinMaxExecutionTuple(): [number, number] { - const executions = this.getData(); + private _getChartLabels(): number[] { + return this.jobExecutions.map((execution) => DateUtil.normalizeToUTC(execution.startTime).getTime()); + } - if (executions.length < 2) { - return [null, null]; - } + private _getChartData(): CustomChartData[] { + const divider = this._getDurationUnit().divider; - return [executions[0].startTime, executions[executions.length - 1].startTime]; + return this.jobExecutions + .map((execution) => { + return { + startTime: DateUtil.normalizeToUTC(execution.startTime).getTime(), + duration: Math.round((this._getJobDurationSeconds(execution) / divider) * 100) / 100, + endTime: execution.endTime ? execution.endTime : undefined, + status: execution.status, + opId: execution.opId, + id: execution.id + } as CustomChartData; + }) + .sort((ex1, ex2) => ex1.startTime - ex2.startTime); } - private _getTimeScaleUnit(min: number | string, max: number | string): TimeUnit { + private _getTimeScaleUnit(chartData: CustomChartData[]): TimeUnit { + const [min, max] = this._getMinMaxExecutionTuple(chartData); + if (CollectionsUtil.isNil(min) || CollectionsUtil.isNil(max)) { return 'day'; } @@ -279,6 +293,119 @@ export class ExecutionDurationChartComponent implements OnInit, OnChanges { return 'millisecond'; } + private _getDurationUnit(): { name: string; divider: number } { + const maxDurationSeconds = this._getMaxDurationSeconds(); + + if (maxDurationSeconds > 60) { + return maxDurationSeconds > 3600 ? { name: 'hours', divider: 3600 } : { name: 'minutes', divider: 60 }; + } else { + return { name: 'seconds', divider: 1 }; + } + } + + private _getMaxDurationSeconds(): number { + return this.jobExecutions + .map((execution) => this._getJobDurationSeconds(execution)) + .sort((v1, v2) => v1 - v2) + .pop(); + } + + private _getJobDurationSeconds(execution: GridDataJobExecution): number { + const endTime = execution.endTime ? new Date(execution.endTime).getTime() : Date.now(); + const delta = endTime - new Date(execution.startTime).getTime(); + + return delta / 1000; + } + + private _emitFocussedExecutionId(activeElements: ActiveElement[]): void { + if (activeElements.length > 0) { + const element: { $context?: { raw?: CustomChartData } } = activeElements[0].element as unknown; + const executionId = element?.$context?.raw?.id ?? null; + + // if event emits that element is focussed and that value is same as previous skip processing + if (this._focusedExecutionId === executionId) { + return; + } + + // when element is focused for the first time, save executionId in component context + this._focusedExecutionId = executionId; + // emit executionId to parent component + this.executionIdFocused.next(executionId); + } else { + // if event emits that no element is focussed and that value is same as previous skip processing + if (!this._focusedExecutionId) { + return; + } + + // when focused element lose focus clear executionId from component context + this._focusedExecutionId = null; + // emit null value to parent component + this.executionIdFocused.next(null); + } + } + + private _getMinMaxExecutionTuple(chartData: CustomChartData[]): [number, number] { + if (chartData.length === 0) { + if (CollectionsUtil.isDate(this._zoomPeriod.from) && CollectionsUtil.isDate(this._zoomPeriod.to)) { + return [this._zoomPeriod.from.getTime(), this._zoomPeriod.to.getTime()]; + } + + return [null, null]; + } + + if (chartData.length === 1) { + if (CollectionsUtil.isDate(this._zoomPeriod.from) && CollectionsUtil.isDate(this._zoomPeriod.to)) { + if (this._zoomPeriod.to.getTime() - this._zoomPeriod.from.getTime() > 5 * this._getTimeUnitMilliseconds('minute')) { + return [this._zoomPeriod.from.getTime(), this._zoomPeriod.to.getTime()]; + } + } + + return [chartData[0].startTime, chartData[0].startTime]; + } + + return [chartData[0].startTime, chartData[chartData.length - 1].startTime]; + } + + private _getMinMaxExecutionTupleAdjusted(chartData: CustomChartData[], unit: TimeUnit): [number, number] { + const [min, max] = this._getMinMaxExecutionTuple(chartData); + + let adjustment: number; + + switch (unit) { + case 'millisecond': + adjustment = 10 * this._getTimeUnitMilliseconds('millisecond'); + break; + case 'second': + adjustment = 5 * this._getTimeUnitMilliseconds('second'); + break; + case 'minute': + adjustment = 5 * this._getTimeUnitMilliseconds('minute'); + break; + case 'hour': + adjustment = 2 * this._getTimeUnitMilliseconds('hour'); + break; + case 'day': + adjustment = 15 * this._getTimeUnitMilliseconds('hour'); + break; + case 'week': + adjustment = 3 * this._getTimeUnitMilliseconds('day'); + break; + case 'month': + adjustment = this._getTimeUnitMilliseconds('month'); + break; + case 'year': + adjustment = this._getTimeUnitMilliseconds('year'); + break; + default: + console.error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Taurus DataPipelines ExecutionDurationChartComponent unsupported time format unit ${unit}` + ); + } + + return [min - adjustment, max + adjustment]; + } + private _getTimeUnitMilliseconds(unit: 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year'): number { switch (unit) { case 'millisecond': diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/executions-filters.model.ts b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/executions-filters.model.ts index ffd947f81a..e89cd1a53f 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/executions-filters.model.ts +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/model/executions-filters.model.ts @@ -11,8 +11,8 @@ export const FILTER_TIME_PERIOD_KEY = 'timePeriod'; export const FILTER_STATUS_KEY = 'status'; export const FILTER_TYPE_KEY = 'type'; export const FILTER_DURATION_KEY = 'duration'; -export const FILTER_START_TIME_KEY = 'startTimeFormatted'; -export const FILTER_END_TIME_KEY = 'endTimeFormatted'; +export const FILTER_START_TIME_KEY = 'startTime'; +export const FILTER_END_TIME_KEY = 'endTime'; export const FILTER_ID_KEY = 'id'; export const FILTER_VERSION_KEY = 'jobVersion'; @@ -32,12 +32,12 @@ export type ExecutionsFilterCriteria = /** * ** Executions filter pair with its corresponding value in Tuple. */ -export type ExecutionsFilterPairs = KeyValueTuple; +export type ExecutionsFilterPairs = KeyValueTuple; /** * ** Executions grid filter with its value. */ -export type ExecutionsGridFilter = { property: ExecutionsFilterCriteria; value: string }; +export type ExecutionsGridFilter = { property: K; value: string }; /** * ** Executions supported filter criteria. diff --git a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/time-period-filter/time-period-filter.component.html b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/time-period-filter/time-period-filter.component.html index ec69f0c914..9c85f4492e 100644 --- a/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/time-period-filter/time-period-filter.component.html +++ b/projects/frontend/data-pipelines/gui/projects/data-pipelines/src/lib/components/data-job/pages/executions/time-period-filter/time-period-filter.component.html @@ -4,7 +4,7 @@ -->
- Time period (UTC): + Filter by time period (UTC): - {{ fromTime | date : "MMM d, y, hh:mm:ss a" : "UTC" }} to {{ toTime - | date : "MMM d, y, hh:mm:ss a" : "UTC" }} + {{ fromDateTime | date : dateTimeFormat : "UTC" }} to {{ toDateTime + | date : dateTimeFormat : "UTC" }} +