From 5802de95654dd3597a605cbfb41e6a55e8818636 Mon Sep 17 00:00:00 2001 From: Daniel Steger Date: Mon, 8 Jun 2020 13:33:46 +0200 Subject: [PATCH 01/14] Created job result module --- frontend/src/app/app-routing.module.ts | 4 +++ .../job-result/job-result.component.html | 3 +++ .../job-result/job-result.component.scss | 0 .../job-result/job-result.component.spec.ts | 25 +++++++++++++++++++ .../job-result/job-result.component.ts | 15 +++++++++++ .../modules/job-result/job-result.module.ts | 18 +++++++++++++ .../threshold-group.component.ts | 4 +-- .../job-threshold/job-threshold.component.ts | 14 +++++------ grails-app/conf/application.yml | 3 ++- .../de/iteratec/osm/UrlMappings.groovy | 1 + 10 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 frontend/src/app/modules/job-result/job-result.component.html create mode 100644 frontend/src/app/modules/job-result/job-result.component.scss create mode 100644 frontend/src/app/modules/job-result/job-result.component.spec.ts create mode 100644 frontend/src/app/modules/job-result/job-result.component.ts create mode 100644 frontend/src/app/modules/job-result/job-result.module.ts diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 5d3715b7a9..77b8f871ca 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -36,6 +36,10 @@ const appRoutes: Routes = [ path: 'distributionDev', loadChildren: './modules/distribution/distribution.module#DistributionModule' }, + { + path: 'jobResultDev', + loadChildren: './modules/job-result/job-result.module#JobResultModule' + }, { path: '', loadChildren: './modules/landing/landing.module#LandingModule', diff --git a/frontend/src/app/modules/job-result/job-result.component.html b/frontend/src/app/modules/job-result/job-result.component.html new file mode 100644 index 0000000000..5134a70b3c --- /dev/null +++ b/frontend/src/app/modules/job-result/job-result.component.html @@ -0,0 +1,3 @@ +

+ job-result works! +

diff --git a/frontend/src/app/modules/job-result/job-result.component.scss b/frontend/src/app/modules/job-result/job-result.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/app/modules/job-result/job-result.component.spec.ts b/frontend/src/app/modules/job-result/job-result.component.spec.ts new file mode 100644 index 0000000000..9ff9f1e692 --- /dev/null +++ b/frontend/src/app/modules/job-result/job-result.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { JobResultComponent } from './job-result.component'; + +describe('JobResultComponent', () => { + let component: JobResultComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ JobResultComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(JobResultComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/modules/job-result/job-result.component.ts b/frontend/src/app/modules/job-result/job-result.component.ts new file mode 100644 index 0000000000..27befb2357 --- /dev/null +++ b/frontend/src/app/modules/job-result/job-result.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'osm-job-result', + templateUrl: './job-result.component.html', + styleUrls: ['./job-result.component.scss'] +}) +export class JobResultComponent implements OnInit { + + constructor() { } + + ngOnInit() { + } + +} diff --git a/frontend/src/app/modules/job-result/job-result.module.ts b/frontend/src/app/modules/job-result/job-result.module.ts new file mode 100644 index 0000000000..f5f879cbe0 --- /dev/null +++ b/frontend/src/app/modules/job-result/job-result.module.ts @@ -0,0 +1,18 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {JobResultComponent} from './job-result.component'; +import {RouterModule, Routes} from '@angular/router'; + +const JobResultRoutes: Routes = [ + {path: 'list', component: JobResultComponent}, +]; + +@NgModule({ + declarations: [JobResultComponent], + imports: [ + CommonModule, + RouterModule.forChild(JobResultRoutes) + ] +}) +export class JobResultModule { +} diff --git a/frontend/src/app/modules/job-threshold/components/threshold-group/threshold-group.component.ts b/frontend/src/app/modules/job-threshold/components/threshold-group/threshold-group.component.ts index cab88bd334..be917738cb 100644 --- a/frontend/src/app/modules/job-threshold/components/threshold-group/threshold-group.component.ts +++ b/frontend/src/app/modules/job-threshold/components/threshold-group/threshold-group.component.ts @@ -17,7 +17,7 @@ export class ThresholdGroupComponent { @Input() unusedMeasuredEvents: MeasuredEvent[]; @Output() removeEvent = new EventEmitter(); @Input() newThreshold: Threshold; - addThresholdDisabled: boolean = false; + addThresholdDisabled = false; unusedMeasurands: Measurand[]; @Input() @@ -28,7 +28,7 @@ export class ThresholdGroupComponent { if (thresholdGroup.isNew) { this.addThreshold(); } - }; + } get thresholdGroup(): ThresholdGroup { return this._thresholdGroup; diff --git a/frontend/src/app/modules/job-threshold/job-threshold.component.ts b/frontend/src/app/modules/job-threshold/job-threshold.component.ts index de02ebe07f..1c98603ee0 100644 --- a/frontend/src/app/modules/job-threshold/job-threshold.component.ts +++ b/frontend/src/app/modules/job-threshold/job-threshold.component.ts @@ -9,7 +9,7 @@ import {MeasurandService} from './services/measurand.service'; @Component({ - selector: 'app-job-threshold', + selector: 'osm-job-threshold', templateUrl: './job-threshold.component.html', styleUrls: ['./job-threshold.component.scss'] }) @@ -19,8 +19,8 @@ export class JobThresholdComponent { jobId: number; scriptId: number; - addMeasuredEventDisabled: boolean = false; - isEmpty: boolean = false; + addMeasuredEventDisabled = false; + isEmpty = false; allThresholdGroups$: Observable; unusedMeasuredEvents$: Observable; @@ -54,7 +54,7 @@ export class JobThresholdComponent { this.measuredEventService.measuredEvents$, this.allThresholdGroups$ ).subscribe(([measuredEvents, thresholdGroups]: [MeasuredEvent[], ThresholdGroup[]]) => { - this.isEmpty = thresholdGroups.length == 0; + this.isEmpty = thresholdGroups.length === 0; this.addMeasuredEventDisabled = thresholdGroups.length === measuredEvents.length; }); @@ -64,10 +64,10 @@ export class JobThresholdComponent { ).pipe( map(([thresholdGroups, measuredEvents]: [ThresholdGroup[], MeasuredEvent[]]) => { return measuredEvents.filter((measuredEvent: MeasuredEvent) => - !thresholdGroups.some(thresholdGroup => thresholdGroup.measuredEvent.id == measuredEvent.id) - ) + !thresholdGroups.some(thresholdGroup => thresholdGroup.measuredEvent.id === measuredEvent.id) + ); }) - ) + ); } addThresholdGroup() { diff --git a/grails-app/conf/application.yml b/grails-app/conf/application.yml index d7866c517e..ef4bbcfc71 100644 --- a/grails-app/conf/application.yml +++ b/grails-app/conf/application.yml @@ -303,8 +303,9 @@ grails: {pattern: '/job/getLastRun', access: ['permitAll']}, {pattern: '/job/getTagsForJobs', access: ['permitAll']}, {pattern: '/job/showLastResultForJob/**', access: ['permitAll']}, - {pattern: '/job/showLastAggregationForJob/**', access: ['permitAll']}, + {pattern: '/job/showLastAggregationForJob/**', access: ['permitAll']}, {pattern: '/jobResult/**', access: ['permitAll']}, + {pattern: '/jobResultDev/**', access: ['permitAll']}, {pattern: '/script/list', access: ['permitAll']}, {pattern: '/register/*', access: ['permitAll']}, {pattern: '/queueStatus/list', access: ['permitAll']}, diff --git a/grails-app/controllers/de/iteratec/osm/UrlMappings.groovy b/grails-app/controllers/de/iteratec/osm/UrlMappings.groovy index b88ab13b98..012ea3e3f5 100755 --- a/grails-app/controllers/de/iteratec/osm/UrlMappings.groovy +++ b/grails-app/controllers/de/iteratec/osm/UrlMappings.groovy @@ -41,6 +41,7 @@ class UrlMappings { "/aspectConfiguration/**?"(view: "/angularFrontend") "/metricFinder/rest/$action"(controller: "metricFinder") "/aspectConfiguration/rest/$action"(controller: "aspectConfiguration") + "/jobResultDev/**"(view: "/angularFrontend") //////////////////////////////////////////////////////////////////////////////////////////////// // Pages with controller From 212e7ae5ebefa4f25b278d8bcb9cfacdcf5d76ce Mon Sep 17 00:00:00 2001 From: Daniel Steger Date: Tue, 9 Jun 2020 15:20:57 +0200 Subject: [PATCH 02/14] Show job results in table --- frontend/package-lock.json | 77 +++++-------------- frontend/src/app/enums/url.enum.ts | 3 +- .../job-result/job-result.component.html | 41 +++++++++- .../job-result/job-result.component.scss | 24 ++++++ .../job-result/job-result.component.spec.ts | 14 +++- .../job-result/job-result.component.ts | 22 +++++- .../modules/job-result/job-result.module.ts | 4 +- .../job-result/models/job-result.model.ts | 9 +++ .../services/job-result-data.service.spec.ts | 12 +++ .../services/job-result-data.service.ts | 30 ++++++++ .../de/iteratec/osm/UrlMappings.groovy | 7 ++ .../osm/result/JobResultController.groovy | 18 ++--- grails-app/i18n/messages.properties | 8 ++ grails-app/i18n/messages_de.properties | 8 ++ grails-app/views/job/_thresholdsTab.gsp | 4 +- ...obResultDTO.groovy => JobResultDTO.groovy} | 6 +- 16 files changed, 206 insertions(+), 81 deletions(-) create mode 100644 frontend/src/app/modules/job-result/models/job-result.model.ts create mode 100644 frontend/src/app/modules/job-result/services/job-result-data.service.spec.ts create mode 100644 frontend/src/app/modules/job-result/services/job-result-data.service.ts rename src/main/groovy/de/iteratec/osm/measurement/schedule/{FailedJobResultDTO.groovy => JobResultDTO.groovy} (82%) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a09e0970dd..bc90f31603 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1455,7 +1455,6 @@ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", "dev": true, - "optional": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -2679,8 +2678,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true + "dev": true }, "constants-browserify": { "version": "1.0.0", @@ -3346,8 +3344,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true, - "optional": true + "dev": true }, "depd": { "version": "1.1.2", @@ -4274,8 +4271,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -4296,14 +4292,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4318,20 +4312,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -4448,8 +4439,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -4461,7 +4451,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4476,7 +4465,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4484,14 +4472,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4510,7 +4496,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -4591,8 +4576,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -4604,7 +4588,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -4690,8 +4673,7 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -4727,7 +4709,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4747,7 +4728,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4791,14 +4771,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -4807,7 +4785,6 @@ "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", "dev": true, - "optional": true, "requires": { "graceful-fs": "^4.1.2", "inherits": "~2.0.0", @@ -4820,7 +4797,6 @@ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "dev": true, - "optional": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -4858,8 +4834,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true, - "optional": true + "dev": true }, "get-stream": { "version": "3.0.0", @@ -5039,8 +5014,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true, - "optional": true + "dev": true }, "has-value": { "version": "1.0.0", @@ -5753,8 +5727,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true, - "optional": true + "dev": true }, "is-windows": { "version": "1.0.2", @@ -6370,7 +6343,6 @@ "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, - "optional": true, "requires": { "graceful-fs": "^4.1.2", "parse-json": "^2.2.0", @@ -6383,8 +6355,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true, - "optional": true + "dev": true } } }, @@ -6668,8 +6639,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true, - "optional": true + "dev": true }, "map-visit": { "version": "1.0.0", @@ -7300,7 +7270,6 @@ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "dev": true, - "optional": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -8198,7 +8167,6 @@ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", "dev": true, - "optional": true, "requires": { "load-json-file": "^1.0.0", "normalize-package-data": "^2.3.2", @@ -8210,7 +8178,6 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", "dev": true, - "optional": true, "requires": { "graceful-fs": "^4.1.2", "pify": "^2.0.0", @@ -8221,8 +8188,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true, - "optional": true + "dev": true } } }, @@ -8231,7 +8197,6 @@ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", "dev": true, - "optional": true, "requires": { "find-up": "^1.0.0", "read-pkg": "^1.0.0" @@ -8242,7 +8207,6 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", "dev": true, - "optional": true, "requires": { "path-exists": "^2.0.0", "pinkie-promise": "^2.0.0" @@ -8253,7 +8217,6 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", "dev": true, - "optional": true, "requires": { "pinkie-promise": "^2.0.0" } @@ -9528,7 +9491,6 @@ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", "dev": true, - "optional": true, "requires": { "is-utf8": "^0.2.0" } @@ -10853,7 +10815,6 @@ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "dev": true, - "optional": true, "requires": { "string-width": "^1.0.2 || 2" } diff --git a/frontend/src/app/enums/url.enum.ts b/frontend/src/app/enums/url.enum.ts index 38f322e451..7b573c0d3a 100644 --- a/frontend/src/app/enums/url.enum.ts +++ b/frontend/src/app/enums/url.enum.ts @@ -10,5 +10,6 @@ export enum URL { RESULT_COUNT = '/resultSelection/getResultCount', AGGREGATION_BARCHART_DATA = '/aggregation/getBarchartData', EVENT_RESULT_DASHBOARD_LINECHART_DATA = '/eventResultDashboard/getLinechartData', - DISTRIBUTION_VIOLINCHART_DATA = '/distributionChart/getViolinchartData' + DISTRIBUTION_VIOLINCHART_DATA = '/distributionChart/getViolinchartData', + JOB_RESULTS = '/jobResult/getJobResults' } diff --git a/frontend/src/app/modules/job-result/job-result.component.html b/frontend/src/app/modules/job-result/job-result.component.html index 5134a70b3c..930d9357c7 100644 --- a/frontend/src/app/modules/job-result/job-result.component.html +++ b/frontend/src/app/modules/job-result/job-result.component.html @@ -1,3 +1,40 @@ -

- job-result works! +

+ {{ 'frontend.de.iteratec.osm.jobResult.title' | translate }} +

+

+ {{ 'frontend.de.iteratec.osm.jobResult.description' | translate }}

+ +
+ No Data + + + + + + + + + + + + + + + + + + + +
+
{{ 'frontend.de.iteratec.osm.jobResult.tableHead.date' | translate }}
+
{{ 'frontend.de.iteratec.osm.jobResult.tableHead.testId' | translate }}
+
{{ 'frontend.de.iteratec.osm.jobResult.tableHead.testAgent' | translate }}{{ 'frontend.de.iteratec.osm.jobResult.tableHead.jobResultStatus' | translate }}{{ 'frontend.de.iteratec.osm.jobResult.tableHead.wptStatus' | translate }}{{ 'frontend.de.iteratec.osm.jobResult.tableHead.description' | translate }}
+ +
{{ test.date }}
+
{{ test.testId }}
+
+ +
{{ test.testAgent ? test.testAgent : '-' }}{{ test.jobResultStatus }}{{ test.wptStatus }}{{ test.description }}
+
diff --git a/frontend/src/app/modules/job-result/job-result.component.scss b/frontend/src/app/modules/job-result/job-result.component.scss index e69de29bb2..296e7c8371 100644 --- a/frontend/src/app/modules/job-result/job-result.component.scss +++ b/frontend/src/app/modules/job-result/job-result.component.scss @@ -0,0 +1,24 @@ +.description { + margin-left: 10px; +} + +.job-id-table-head { + font-style: italic; + font-weight: normal; +} + +.job-status-successful { + color: #2ECC71; +} + +.job-status-error { + color: #EB5E55; +} + +.job-date { + font-weight: bold; +} + +.job-id { + font-style: italic; +} diff --git a/frontend/src/app/modules/job-result/job-result.component.spec.ts b/frontend/src/app/modules/job-result/job-result.component.spec.ts index 9ff9f1e692..28763628c5 100644 --- a/frontend/src/app/modules/job-result/job-result.component.spec.ts +++ b/frontend/src/app/modules/job-result/job-result.component.spec.ts @@ -1,6 +1,8 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import { JobResultComponent } from './job-result.component'; +import {JobResultComponent} from './job-result.component'; +import {SharedModule} from '../shared/shared.module'; +import {SharedMocksModule} from '../../testing/shared-mocks.module'; describe('JobResultComponent', () => { let component: JobResultComponent; @@ -8,9 +10,13 @@ describe('JobResultComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ JobResultComponent ] + declarations: [JobResultComponent], + imports: [ + SharedModule, + SharedMocksModule + ] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { diff --git a/frontend/src/app/modules/job-result/job-result.component.ts b/frontend/src/app/modules/job-result/job-result.component.ts index 27befb2357..1e28d671e6 100644 --- a/frontend/src/app/modules/job-result/job-result.component.ts +++ b/frontend/src/app/modules/job-result/job-result.component.ts @@ -1,4 +1,6 @@ -import { Component, OnInit } from '@angular/core'; +import {Component, OnInit} from '@angular/core'; +import {JobResultDataService} from './services/job-result-data.service'; +import {JobResult} from './models/job-result.model'; @Component({ selector: 'osm-job-result', @@ -7,9 +9,25 @@ import { Component, OnInit } from '@angular/core'; }) export class JobResultComponent implements OnInit { - constructor() { } + jobResults: JobResult[] = []; + + constructor(private dataService: JobResultDataService) { + } ngOnInit() { + this.getJobResults(); + } + + getJobResults(): void { + this.dataService.getJobResults(1065) + .subscribe((jobResults: JobResult[]) => this.jobResults = jobResults); + } + + isTestSuccessful(test: JobResult): boolean { + return test.jobResultStatus === 'Finished' && test.wptStatus === 'COMPLETED (200)' && test.description === 'Ok'; + } + + sort(event: any) { } } diff --git a/frontend/src/app/modules/job-result/job-result.module.ts b/frontend/src/app/modules/job-result/job-result.module.ts index f5f879cbe0..52305cc8c4 100644 --- a/frontend/src/app/modules/job-result/job-result.module.ts +++ b/frontend/src/app/modules/job-result/job-result.module.ts @@ -2,6 +2,7 @@ import {NgModule} from '@angular/core'; import {CommonModule} from '@angular/common'; import {JobResultComponent} from './job-result.component'; import {RouterModule, Routes} from '@angular/router'; +import {SharedModule} from '../shared/shared.module'; const JobResultRoutes: Routes = [ {path: 'list', component: JobResultComponent}, @@ -11,7 +12,8 @@ const JobResultRoutes: Routes = [ declarations: [JobResultComponent], imports: [ CommonModule, - RouterModule.forChild(JobResultRoutes) + RouterModule.forChild(JobResultRoutes), + SharedModule ] }) export class JobResultModule { diff --git a/frontend/src/app/modules/job-result/models/job-result.model.ts b/frontend/src/app/modules/job-result/models/job-result.model.ts new file mode 100644 index 0000000000..58693abf2e --- /dev/null +++ b/frontend/src/app/modules/job-result/models/job-result.model.ts @@ -0,0 +1,9 @@ +export interface JobResult { + testId: string; + testUrl: string; + testAgent: string; + date: Date; + jobResultStatus: string; + wptStatus: string; + description: string; +} diff --git a/frontend/src/app/modules/job-result/services/job-result-data.service.spec.ts b/frontend/src/app/modules/job-result/services/job-result-data.service.spec.ts new file mode 100644 index 0000000000..483d3e1d95 --- /dev/null +++ b/frontend/src/app/modules/job-result/services/job-result-data.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { JobResultDataService } from './job-result-data.service'; + +describe('JobResultDataService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: JobResultDataService = TestBed.get(JobResultDataService); + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/modules/job-result/services/job-result-data.service.ts b/frontend/src/app/modules/job-result/services/job-result-data.service.ts new file mode 100644 index 0000000000..dc10ef339f --- /dev/null +++ b/frontend/src/app/modules/job-result/services/job-result-data.service.ts @@ -0,0 +1,30 @@ +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {Observable, of} from 'rxjs'; +import {URL} from '../../../enums/url.enum'; +import {catchError, tap} from 'rxjs/operators'; +import {JobResult} from '../models/job-result.model'; + +@Injectable({ + providedIn: 'root' +}) +export class JobResultDataService { + + constructor(private http: HttpClient) { + } + + getJobResults(jobId: number): Observable { + const params = {jobId: jobId.toString(10)}; + return this.http.get(URL.JOB_RESULTS, {params: params}) + .pipe( + catchError(this.handleError('getJobResults', [])) + ); + } + + private handleError(operation = 'operation', result?: T) { + return (error: any): Observable => { + console.error(operation, error.message); + return of(result as T); + }; + } +} diff --git a/grails-app/controllers/de/iteratec/osm/UrlMappings.groovy b/grails-app/controllers/de/iteratec/osm/UrlMappings.groovy index 012ea3e3f5..7751a82836 100755 --- a/grails-app/controllers/de/iteratec/osm/UrlMappings.groovy +++ b/grails-app/controllers/de/iteratec/osm/UrlMappings.groovy @@ -219,5 +219,12 @@ class UrlMappings { controller = "distribution" action = [GET: "getDistributionChartData"] } + + // JobResultController ////////////////////////////////////////// + + "/jobResult/getJobResults" { + controller = "jobResult" + action = [GET: "getJobResults"] + } } } diff --git a/grails-app/controllers/de/iteratec/osm/result/JobResultController.groovy b/grails-app/controllers/de/iteratec/osm/result/JobResultController.groovy index 1125b5c1f5..4937ff2755 100644 --- a/grails-app/controllers/de/iteratec/osm/result/JobResultController.groovy +++ b/grails-app/controllers/de/iteratec/osm/result/JobResultController.groovy @@ -1,6 +1,7 @@ package de.iteratec.osm.result -import de.iteratec.osm.measurement.schedule.FailedJobResultDTO +import de.iteratec.osm.annotations.RestAction +import de.iteratec.osm.measurement.schedule.JobResultDTO import de.iteratec.osm.measurement.schedule.Job import de.iteratec.osm.measurement.schedule.JobDaoService import de.iteratec.osm.measurement.schedule.JobStatisticService @@ -23,22 +24,21 @@ class JobResultController { return ['allJobs': allJobs, 'selectedJobId': selectedJobId] } + @RestAction def getJobResults(Long jobId) { - List failedJobResults + List jobResultsForJob Job job = Job.get(jobId) if (job) { - List jobResultsForJob = jobStatisticService.getLast150CompletedJobResultsFor(job) - - failedJobResults = jobResultsForJob.findAll { it.jobResultStatus.isFailed() } + jobResultsForJob = jobStatisticService.getLast150CompletedJobResultsFor(job) } - List dtos = [] - failedJobResults.each { - FailedJobResultDTO jobResultDTO = new FailedJobResultDTO(it) + List dtos = [] + jobResultsForJob.each { + JobResultDTO jobResultDTO = new JobResultDTO(it) jobResultDTO.date = new SimpleDateFormat(i18nService.msg("default.date.format.medium", "yyyy-MM-dd")).format(it.date) dtos << jobResultDTO } - ControllerUtils.sendObjectAsJSON(response, ['jobLabel': job.label, 'jobResults': dtos]) + ControllerUtils.sendObjectAsJSON(response, dtos) } } diff --git a/grails-app/i18n/messages.properties b/grails-app/i18n/messages.properties index f6ffc04fbd..59173e804e 100644 --- a/grails-app/i18n/messages.properties +++ b/grails-app/i18n/messages.properties @@ -1313,3 +1313,11 @@ frontend.de.iteratec.osm.distribution.chart.settings.sort=Sort frontend.de.iteratec.osm.distribution.chart.settings.sort.desc=Descending Median frontend.de.iteratec.osm.distribution.chart.settings.sort.asc=Ascending Median frontend.de.iteratec.osm.distribution.chart.settings.customerJourney=Customer Journey +frontend.de.iteratec.osm.jobResult.title=Measurements +frontend.de.iteratec.osm.jobResult.description=Tests of the last 150 job runs +frontend.de.iteratec.osm.jobResult.tableHead.date=Date +frontend.de.iteratec.osm.jobResult.tableHead.testId=Test ID +frontend.de.iteratec.osm.jobResult.tableHead.testAgent=Test agent +frontend.de.iteratec.osm.jobResult.tableHead.jobResultStatus=Status +frontend.de.iteratec.osm.jobResult.tableHead.wptStatus=WPT status +frontend.de.iteratec.osm.jobResult.tableHead.description=Description diff --git a/grails-app/i18n/messages_de.properties b/grails-app/i18n/messages_de.properties index 5bd5dff485..cfa203fc31 100644 --- a/grails-app/i18n/messages_de.properties +++ b/grails-app/i18n/messages_de.properties @@ -1287,3 +1287,11 @@ frontend.de.iteratec.osm.distribution.chart.settings.sort=Sortierung frontend.de.iteratec.osm.distribution.chart.settings.sort.desc=Absteigender Median frontend.de.iteratec.osm.distribution.chart.settings.sort.asc=Aufsteigender Median frontend.de.iteratec.osm.distribution.chart.settings.customerJourney=Customer Journey +frontend.de.iteratec.osm.jobResult.title=Messungen +frontend.de.iteratec.osm.jobResult.description=Messungen der letzten 150 Jobausführungen +frontend.de.iteratec.osm.jobResult.tableHead.date=Datum +frontend.de.iteratec.osm.jobResult.tableHead.testId=Test ID +frontend.de.iteratec.osm.jobResult.tableHead.testAgent=Testagent +frontend.de.iteratec.osm.jobResult.tableHead.jobResultStatus=Status +frontend.de.iteratec.osm.jobResult.tableHead.wptStatus=WPT-Status +frontend.de.iteratec.osm.jobResult.tableHead.description=Beschreibung diff --git a/grails-app/views/job/_thresholdsTab.gsp b/grails-app/views/job/_thresholdsTab.gsp index 4640fa7b5c..19d6bb87d5 100644 --- a/grails-app/views/job/_thresholdsTab.gsp +++ b/grails-app/views/job/_thresholdsTab.gsp @@ -1,8 +1,8 @@
- + data-module-path="src/app/modules/job-threshold/job-threshold.module#ThresholdModule">
diff --git a/src/main/groovy/de/iteratec/osm/measurement/schedule/FailedJobResultDTO.groovy b/src/main/groovy/de/iteratec/osm/measurement/schedule/JobResultDTO.groovy similarity index 82% rename from src/main/groovy/de/iteratec/osm/measurement/schedule/FailedJobResultDTO.groovy rename to src/main/groovy/de/iteratec/osm/measurement/schedule/JobResultDTO.groovy index 29a1d416fc..5df810c063 100644 --- a/src/main/groovy/de/iteratec/osm/measurement/schedule/FailedJobResultDTO.groovy +++ b/src/main/groovy/de/iteratec/osm/measurement/schedule/JobResultDTO.groovy @@ -4,16 +4,18 @@ import de.iteratec.osm.result.JobResult import java.text.SimpleDateFormat -class FailedJobResultDTO { +class JobResultDTO { String testId + String testAgent String date String jobResultStatus String wptStatus String description URL testUrl - FailedJobResultDTO(JobResult jobResult) { + JobResultDTO(JobResult jobResult) { testId = jobResult.testId + testAgent = jobResult.testAgent date = new SimpleDateFormat().format(jobResult.date) jobResultStatus = jobResult.jobResultStatus.getMessage() wptStatus = jobResult.wptStatus.getMessage() From f980f710ce3e97755a3bc83582f32a671e9fc0fb Mon Sep 17 00:00:00 2001 From: Daniel Steger Date: Tue, 9 Jun 2020 17:13:45 +0200 Subject: [PATCH 03/14] Added sorting function for table columns --- .../job-result/job-result.component.html | 60 +++++++++++++++---- .../job-result/job-result.component.scss | 12 ++-- .../job-result/job-result.component.ts | 40 ++++++++++++- grails-app/i18n/messages.properties | 3 +- grails-app/i18n/messages_de.properties | 3 +- 5 files changed, 101 insertions(+), 17 deletions(-) diff --git a/frontend/src/app/modules/job-result/job-result.component.html b/frontend/src/app/modules/job-result/job-result.component.html index 930d9357c7..49ce3de7e4 100644 --- a/frontend/src/app/modules/job-result/job-result.component.html +++ b/frontend/src/app/modules/job-result/job-result.component.html @@ -5,25 +5,65 @@

{{ 'frontend.de.iteratec.osm.jobResult.description' | translate }}

-
- No Data +
+ {{ 'frontend.de.iteratec.osm.jobResult.noData' | translate }} - + + + + - - - -
-
{{ 'frontend.de.iteratec.osm.jobResult.tableHead.date' | translate }}
-
{{ 'frontend.de.iteratec.osm.jobResult.tableHead.testId' | translate }}
+
+
+ {{ 'frontend.de.iteratec.osm.jobResult.tableHead.date' | translate }} + + +
+
+ {{ 'frontend.de.iteratec.osm.jobResult.tableHead.testId' | translate }} +
+
+ + {{ 'frontend.de.iteratec.osm.jobResult.tableHead.testAgent' | translate }} + + + + + + {{ 'frontend.de.iteratec.osm.jobResult.tableHead.jobResultStatus' | translate }} + + + + + + {{ 'frontend.de.iteratec.osm.jobResult.tableHead.wptStatus' | translate }} + + + + + + {{ 'frontend.de.iteratec.osm.jobResult.tableHead.description' | translate }} + + + {{ 'frontend.de.iteratec.osm.jobResult.tableHead.testAgent' | translate }}{{ 'frontend.de.iteratec.osm.jobResult.tableHead.jobResultStatus' | translate }}{{ 'frontend.de.iteratec.osm.jobResult.tableHead.wptStatus' | translate }}{{ 'frontend.de.iteratec.osm.jobResult.tableHead.description' | translate }}
-
{{ test.date }}
{{ test.testId }}
diff --git a/frontend/src/app/modules/job-result/job-result.component.scss b/frontend/src/app/modules/job-result/job-result.component.scss index 296e7c8371..753b87db4b 100644 --- a/frontend/src/app/modules/job-result/job-result.component.scss +++ b/frontend/src/app/modules/job-result/job-result.component.scss @@ -2,15 +2,19 @@ margin-left: 10px; } +.align-center { + text-align: center; +} + +.sortable { + cursor: pointer; +} + .job-id-table-head { font-style: italic; font-weight: normal; } -.job-status-successful { - color: #2ECC71; -} - .job-status-error { color: #EB5E55; } diff --git a/frontend/src/app/modules/job-result/job-result.component.ts b/frontend/src/app/modules/job-result/job-result.component.ts index 1e28d671e6..bba4851578 100644 --- a/frontend/src/app/modules/job-result/job-result.component.ts +++ b/frontend/src/app/modules/job-result/job-result.component.ts @@ -10,6 +10,7 @@ import {JobResult} from './models/job-result.model'; export class JobResultComponent implements OnInit { jobResults: JobResult[] = []; + currentSortingRule: { [key: string]: string } = {column: 'date', direction: 'desc'}; constructor(private dataService: JobResultDataService) { } @@ -27,7 +28,44 @@ export class JobResultComponent implements OnInit { return test.jobResultStatus === 'Finished' && test.wptStatus === 'COMPLETED (200)' && test.description === 'Ok'; } - sort(event: any) { + sort(column: string) { + if (column === this.currentSortingRule.column) { + if (this.currentSortingRule.direction === 'desc') { + this.currentSortingRule.direction = 'asc'; + } else if (this.currentSortingRule.direction === 'asc') { + this.currentSortingRule.direction = 'desc'; + } + } else { + this.currentSortingRule.column = column; + this.currentSortingRule.direction = 'asc'; + } + + this.jobResults.sort((a: JobResult, b: JobResult) => + this.compareJobResults(a, b, this.currentSortingRule.column, this.currentSortingRule.direction) + ); } + private compareJobResults(a: JobResult, b: JobResult, column: string, direction: string): number { + let directionFactor = 1; + if (direction === 'desc') { + directionFactor = -1; + } + + if (a[column] && b[column]) { + if (a[column] < b[column]) { + return -directionFactor; + } + if (a[column] > b[column]) { + return directionFactor; + } + } else { + if (a[column]) { + return directionFactor; + } + if (b[column]) { + return -directionFactor; + } + } + return 0; + } } diff --git a/grails-app/i18n/messages.properties b/grails-app/i18n/messages.properties index 59173e804e..92d196dd10 100644 --- a/grails-app/i18n/messages.properties +++ b/grails-app/i18n/messages.properties @@ -1314,7 +1314,8 @@ frontend.de.iteratec.osm.distribution.chart.settings.sort.desc=Descending Median frontend.de.iteratec.osm.distribution.chart.settings.sort.asc=Ascending Median frontend.de.iteratec.osm.distribution.chart.settings.customerJourney=Customer Journey frontend.de.iteratec.osm.jobResult.title=Measurements -frontend.de.iteratec.osm.jobResult.description=Tests of the last 150 job runs +frontend.de.iteratec.osm.jobResult.description=The last 150 job runs +frontend.de.iteratec.osm.jobResult.noData=No measurements frontend.de.iteratec.osm.jobResult.tableHead.date=Date frontend.de.iteratec.osm.jobResult.tableHead.testId=Test ID frontend.de.iteratec.osm.jobResult.tableHead.testAgent=Test agent diff --git a/grails-app/i18n/messages_de.properties b/grails-app/i18n/messages_de.properties index cfa203fc31..e324bf089b 100644 --- a/grails-app/i18n/messages_de.properties +++ b/grails-app/i18n/messages_de.properties @@ -1288,7 +1288,8 @@ frontend.de.iteratec.osm.distribution.chart.settings.sort.desc=Absteigender Medi frontend.de.iteratec.osm.distribution.chart.settings.sort.asc=Aufsteigender Median frontend.de.iteratec.osm.distribution.chart.settings.customerJourney=Customer Journey frontend.de.iteratec.osm.jobResult.title=Messungen -frontend.de.iteratec.osm.jobResult.description=Messungen der letzten 150 Jobausführungen +frontend.de.iteratec.osm.jobResult.description=Die letzten 150 Jobausführungen +frontend.de.iteratec.osm.jobResult.noData=Keine Messungen frontend.de.iteratec.osm.jobResult.tableHead.date=Datum frontend.de.iteratec.osm.jobResult.tableHead.testId=Test ID frontend.de.iteratec.osm.jobResult.tableHead.testAgent=Testagent From 33a930cd174490efa6d053476643bc8c2f27b4ff Mon Sep 17 00:00:00 2001 From: Daniel Steger Date: Wed, 10 Jun 2020 17:47:23 +0200 Subject: [PATCH 04/14] Execution dates in Date format instead of string. Get all jobs. --- frontend/src/app/enums/url.enum.ts | 1 + .../modules/job-result/job-result.component.html | 3 ++- .../app/modules/job-result/job-result.component.ts | 7 +++++++ .../services/job-result-data.service.spec.ts | 9 ++++++--- .../job-result/services/job-result-data.service.ts | 13 ++++++++++++- .../controllers/de/iteratec/osm/UrlMappings.groovy | 5 +++++ .../iteratec/osm/result/JobResultController.groovy | 9 ++++++++- grails-app/i18n/messages.properties | 3 ++- grails-app/i18n/messages_de.properties | 3 ++- 9 files changed, 45 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/enums/url.enum.ts b/frontend/src/app/enums/url.enum.ts index 7b573c0d3a..0c067ed4b2 100644 --- a/frontend/src/app/enums/url.enum.ts +++ b/frontend/src/app/enums/url.enum.ts @@ -11,5 +11,6 @@ export enum URL { AGGREGATION_BARCHART_DATA = '/aggregation/getBarchartData', EVENT_RESULT_DASHBOARD_LINECHART_DATA = '/eventResultDashboard/getLinechartData', DISTRIBUTION_VIOLINCHART_DATA = '/distributionChart/getViolinchartData', + ALL_JOBS = '/jobResult/getAllJobs', JOB_RESULTS = '/jobResult/getJobResults' } diff --git a/frontend/src/app/modules/job-result/job-result.component.html b/frontend/src/app/modules/job-result/job-result.component.html index 49ce3de7e4..b01a70fd3d 100644 --- a/frontend/src/app/modules/job-result/job-result.component.html +++ b/frontend/src/app/modules/job-result/job-result.component.html @@ -65,7 +65,8 @@

-
{{ test.date }}
+
{{ test.date | date:('frontend.de.iteratec.osm.jobResult.dateFormat' | translate) }}
{{ test.testId }}
diff --git a/frontend/src/app/modules/job-result/job-result.component.ts b/frontend/src/app/modules/job-result/job-result.component.ts index bba4851578..c531d78229 100644 --- a/frontend/src/app/modules/job-result/job-result.component.ts +++ b/frontend/src/app/modules/job-result/job-result.component.ts @@ -9,6 +9,7 @@ import {JobResult} from './models/job-result.model'; }) export class JobResultComponent implements OnInit { + jobs: { [key: number]: string } = {}; jobResults: JobResult[] = []; currentSortingRule: { [key: string]: string } = {column: 'date', direction: 'desc'}; @@ -16,9 +17,15 @@ export class JobResultComponent implements OnInit { } ngOnInit() { + this.getAllJobs(); this.getJobResults(); } + getAllJobs(): void { + this.dataService.getAllJobs() + .subscribe((jobs: { [key: number]: string }) => this.jobs = jobs); + } + getJobResults(): void { this.dataService.getJobResults(1065) .subscribe((jobResults: JobResult[]) => this.jobResults = jobResults); diff --git a/frontend/src/app/modules/job-result/services/job-result-data.service.spec.ts b/frontend/src/app/modules/job-result/services/job-result-data.service.spec.ts index 483d3e1d95..7a9be4fa2f 100644 --- a/frontend/src/app/modules/job-result/services/job-result-data.service.spec.ts +++ b/frontend/src/app/modules/job-result/services/job-result-data.service.spec.ts @@ -1,9 +1,12 @@ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { JobResultDataService } from './job-result-data.service'; +import {JobResultDataService} from './job-result-data.service'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; describe('JobResultDataService', () => { - beforeEach(() => TestBed.configureTestingModule({})); + beforeEach(() => TestBed.configureTestingModule({ + imports: [HttpClientTestingModule] + })); it('should be created', () => { const service: JobResultDataService = TestBed.get(JobResultDataService); diff --git a/frontend/src/app/modules/job-result/services/job-result-data.service.ts b/frontend/src/app/modules/job-result/services/job-result-data.service.ts index dc10ef339f..2f5dbb4dd1 100644 --- a/frontend/src/app/modules/job-result/services/job-result-data.service.ts +++ b/frontend/src/app/modules/job-result/services/job-result-data.service.ts @@ -2,7 +2,7 @@ import {Injectable} from '@angular/core'; import {HttpClient} from '@angular/common/http'; import {Observable, of} from 'rxjs'; import {URL} from '../../../enums/url.enum'; -import {catchError, tap} from 'rxjs/operators'; +import {catchError, map} from 'rxjs/operators'; import {JobResult} from '../models/job-result.model'; @Injectable({ @@ -13,10 +13,21 @@ export class JobResultDataService { constructor(private http: HttpClient) { } + getAllJobs(): Observable { + return this.http.get(URL.ALL_JOBS) + .pipe( + catchError(this.handleError('getJobResults', [])) + ); + } + getJobResults(jobId: number): Observable { const params = {jobId: jobId.toString(10)}; return this.http.get(URL.JOB_RESULTS, {params: params}) .pipe( + map((data: JobResult[]) => { + data.forEach((jobResult: JobResult) => jobResult.date = new Date(jobResult.date)); + return data; + }), catchError(this.handleError('getJobResults', [])) ); } diff --git a/grails-app/controllers/de/iteratec/osm/UrlMappings.groovy b/grails-app/controllers/de/iteratec/osm/UrlMappings.groovy index 7751a82836..d317ee7439 100755 --- a/grails-app/controllers/de/iteratec/osm/UrlMappings.groovy +++ b/grails-app/controllers/de/iteratec/osm/UrlMappings.groovy @@ -222,6 +222,11 @@ class UrlMappings { // JobResultController ////////////////////////////////////////// + "/jobResult/getAllJobs" { + controller = "jobResult" + action = [GET: "getAllJobs"] + } + "/jobResult/getJobResults" { controller = "jobResult" action = [GET: "getJobResults"] diff --git a/grails-app/controllers/de/iteratec/osm/result/JobResultController.groovy b/grails-app/controllers/de/iteratec/osm/result/JobResultController.groovy index 4937ff2755..7d49e40287 100644 --- a/grails-app/controllers/de/iteratec/osm/result/JobResultController.groovy +++ b/grails-app/controllers/de/iteratec/osm/result/JobResultController.groovy @@ -24,6 +24,13 @@ class JobResultController { return ['allJobs': allJobs, 'selectedJobId': selectedJobId] } + @RestAction + def getAllJobs() { + Map allJobs = jobDaoService.getJobLabels() + + return ControllerUtils.sendObjectAsJSON(response, allJobs) + } + @RestAction def getJobResults(Long jobId) { List jobResultsForJob @@ -35,7 +42,7 @@ class JobResultController { List dtos = [] jobResultsForJob.each { JobResultDTO jobResultDTO = new JobResultDTO(it) - jobResultDTO.date = new SimpleDateFormat(i18nService.msg("default.date.format.medium", "yyyy-MM-dd")).format(it.date) + jobResultDTO.date = it.date.toString() dtos << jobResultDTO } diff --git a/grails-app/i18n/messages.properties b/grails-app/i18n/messages.properties index 92d196dd10..d9a06f2ef7 100644 --- a/grails-app/i18n/messages.properties +++ b/grails-app/i18n/messages.properties @@ -1317,8 +1317,9 @@ frontend.de.iteratec.osm.jobResult.title=Measurements frontend.de.iteratec.osm.jobResult.description=The last 150 job runs frontend.de.iteratec.osm.jobResult.noData=No measurements frontend.de.iteratec.osm.jobResult.tableHead.date=Date -frontend.de.iteratec.osm.jobResult.tableHead.testId=Test ID +frontend.de.iteratec.osm.jobResult.tableHead.testId=Job ID frontend.de.iteratec.osm.jobResult.tableHead.testAgent=Test agent frontend.de.iteratec.osm.jobResult.tableHead.jobResultStatus=Status frontend.de.iteratec.osm.jobResult.tableHead.wptStatus=WPT status frontend.de.iteratec.osm.jobResult.tableHead.description=Description +frontend.de.iteratec.osm.jobResult.dateFormat=yyyy-MM-dd hh:mm aa diff --git a/grails-app/i18n/messages_de.properties b/grails-app/i18n/messages_de.properties index e324bf089b..a6d2b408c6 100644 --- a/grails-app/i18n/messages_de.properties +++ b/grails-app/i18n/messages_de.properties @@ -1291,8 +1291,9 @@ frontend.de.iteratec.osm.jobResult.title=Messungen frontend.de.iteratec.osm.jobResult.description=Die letzten 150 Jobausführungen frontend.de.iteratec.osm.jobResult.noData=Keine Messungen frontend.de.iteratec.osm.jobResult.tableHead.date=Datum -frontend.de.iteratec.osm.jobResult.tableHead.testId=Test ID +frontend.de.iteratec.osm.jobResult.tableHead.testId=Job ID frontend.de.iteratec.osm.jobResult.tableHead.testAgent=Testagent frontend.de.iteratec.osm.jobResult.tableHead.jobResultStatus=Status frontend.de.iteratec.osm.jobResult.tableHead.wptStatus=WPT-Status frontend.de.iteratec.osm.jobResult.tableHead.description=Beschreibung +frontend.de.iteratec.osm.jobResult.dateFormat=dd.MM.yyyy HH:mm From c384294b4c7a34bf656faab6d2c83bacaf2bb499 Mon Sep 17 00:00:00 2001 From: Daniel Steger Date: Thu, 11 Jun 2020 17:35:56 +0200 Subject: [PATCH 05/14] Get all jobs --- .../job-result/job-result.component.html | 21 ++++++++++++++++--- .../job-result/job-result.component.scss | 4 ---- .../job-result/job-result.component.ts | 12 ++++++----- .../modules/job-result/job-result.module.ts | 14 ++++++++----- .../modules/job-result/models/job.model.ts | 9 ++++++++ .../services/job-result-data.service.ts | 16 +++++++++++--- .../osm/result/JobResultController.groovy | 4 ++-- grails-app/i18n/messages.properties | 6 ++++++ grails-app/i18n/messages_de.properties | 6 ++++++ .../measurement/schedule/JobDaoService.groovy | 16 +++++++++++++- 10 files changed, 85 insertions(+), 23 deletions(-) create mode 100644 frontend/src/app/modules/job-result/models/job.model.ts diff --git a/frontend/src/app/modules/job-result/job-result.component.html b/frontend/src/app/modules/job-result/job-result.component.html index b01a70fd3d..5f08027c3f 100644 --- a/frontend/src/app/modules/job-result/job-result.component.html +++ b/frontend/src/app/modules/job-result/job-result.component.html @@ -5,9 +5,24 @@

{{ 'frontend.de.iteratec.osm.jobResult.description' | translate }}

-
- {{ 'frontend.de.iteratec.osm.jobResult.noData' | translate }} - +
+
+ + + +
+ + + +
diff --git a/frontend/src/app/modules/job-result/job-result.component.scss b/frontend/src/app/modules/job-result/job-result.component.scss index 753b87db4b..c010a70fd4 100644 --- a/frontend/src/app/modules/job-result/job-result.component.scss +++ b/frontend/src/app/modules/job-result/job-result.component.scss @@ -2,10 +2,6 @@ margin-left: 10px; } -.align-center { - text-align: center; -} - .sortable { cursor: pointer; } diff --git a/frontend/src/app/modules/job-result/job-result.component.ts b/frontend/src/app/modules/job-result/job-result.component.ts index c531d78229..07d8551129 100644 --- a/frontend/src/app/modules/job-result/job-result.component.ts +++ b/frontend/src/app/modules/job-result/job-result.component.ts @@ -1,6 +1,7 @@ import {Component, OnInit} from '@angular/core'; import {JobResultDataService} from './services/job-result-data.service'; import {JobResult} from './models/job-result.model'; +import {Job} from './models/job.model'; @Component({ selector: 'osm-job-result', @@ -9,8 +10,10 @@ import {JobResult} from './models/job-result.model'; }) export class JobResultComponent implements OnInit { - jobs: { [key: number]: string } = {}; + jobs: Job[] = []; jobResults: JobResult[] = []; + + selectedJob: Job = null; currentSortingRule: { [key: string]: string } = {column: 'date', direction: 'desc'}; constructor(private dataService: JobResultDataService) { @@ -18,16 +21,15 @@ export class JobResultComponent implements OnInit { ngOnInit() { this.getAllJobs(); - this.getJobResults(); } getAllJobs(): void { this.dataService.getAllJobs() - .subscribe((jobs: { [key: number]: string }) => this.jobs = jobs); + .subscribe((jobs: Job[]) => this.jobs = jobs); } - getJobResults(): void { - this.dataService.getJobResults(1065) + getJobResults(jobId: number): void { + this.dataService.getJobResults(jobId) .subscribe((jobResults: JobResult[]) => this.jobResults = jobResults); } diff --git a/frontend/src/app/modules/job-result/job-result.module.ts b/frontend/src/app/modules/job-result/job-result.module.ts index 52305cc8c4..1806114364 100644 --- a/frontend/src/app/modules/job-result/job-result.module.ts +++ b/frontend/src/app/modules/job-result/job-result.module.ts @@ -3,6 +3,8 @@ import {CommonModule} from '@angular/common'; import {JobResultComponent} from './job-result.component'; import {RouterModule, Routes} from '@angular/router'; import {SharedModule} from '../shared/shared.module'; +import {FormsModule} from '@angular/forms'; +import {NgSelectModule} from '@ng-select/ng-select'; const JobResultRoutes: Routes = [ {path: 'list', component: JobResultComponent}, @@ -10,11 +12,13 @@ const JobResultRoutes: Routes = [ @NgModule({ declarations: [JobResultComponent], - imports: [ - CommonModule, - RouterModule.forChild(JobResultRoutes), - SharedModule - ] + imports: [ + CommonModule, + RouterModule.forChild(JobResultRoutes), + SharedModule, + FormsModule, + NgSelectModule + ] }) export class JobResultModule { } diff --git a/frontend/src/app/modules/job-result/models/job.model.ts b/frontend/src/app/modules/job-result/models/job.model.ts new file mode 100644 index 0000000000..541acc8cc7 --- /dev/null +++ b/frontend/src/app/modules/job-result/models/job.model.ts @@ -0,0 +1,9 @@ +export interface Job { + id: number; + label: string; + script: string; + location: any; + lastRun: Date; + lastChange: Date; + description: string; +} diff --git a/frontend/src/app/modules/job-result/services/job-result-data.service.ts b/frontend/src/app/modules/job-result/services/job-result-data.service.ts index 2f5dbb4dd1..fc37899a47 100644 --- a/frontend/src/app/modules/job-result/services/job-result-data.service.ts +++ b/frontend/src/app/modules/job-result/services/job-result-data.service.ts @@ -4,6 +4,7 @@ import {Observable, of} from 'rxjs'; import {URL} from '../../../enums/url.enum'; import {catchError, map} from 'rxjs/operators'; import {JobResult} from '../models/job-result.model'; +import {Job} from '../models/job.model'; @Injectable({ providedIn: 'root' @@ -13,10 +14,19 @@ export class JobResultDataService { constructor(private http: HttpClient) { } - getAllJobs(): Observable { - return this.http.get(URL.ALL_JOBS) + getAllJobs(): Observable { + return this.http.get(URL.ALL_JOBS) .pipe( - catchError(this.handleError('getJobResults', [])) + map((data: Job[]) => { + data.forEach((job: Job) => { + job.id = job[0]; + job.label = job[1]; + delete job[0]; + delete job[1]; + }); + return data; + }), + catchError(this.handleError('getAllJobs', [])) ); } diff --git a/grails-app/controllers/de/iteratec/osm/result/JobResultController.groovy b/grails-app/controllers/de/iteratec/osm/result/JobResultController.groovy index 7d49e40287..c3f54de6ec 100644 --- a/grails-app/controllers/de/iteratec/osm/result/JobResultController.groovy +++ b/grails-app/controllers/de/iteratec/osm/result/JobResultController.groovy @@ -17,7 +17,7 @@ class JobResultController { I18nService i18nService def listFailed(Long jobId) { - Map allJobs = jobDaoService.getJobLabels() + Map allJobs = jobDaoService.getJobLabelsAsMap() String selectedJobId = jobId ?: '' @@ -26,7 +26,7 @@ class JobResultController { @RestAction def getAllJobs() { - Map allJobs = jobDaoService.getJobLabels() + List allJobs = jobDaoService.getJobLabels() return ControllerUtils.sendObjectAsJSON(response, allJobs) } diff --git a/grails-app/i18n/messages.properties b/grails-app/i18n/messages.properties index d9a06f2ef7..5620d86e09 100644 --- a/grails-app/i18n/messages.properties +++ b/grails-app/i18n/messages.properties @@ -1315,6 +1315,12 @@ frontend.de.iteratec.osm.distribution.chart.settings.sort.asc=Ascending Median frontend.de.iteratec.osm.distribution.chart.settings.customerJourney=Customer Journey frontend.de.iteratec.osm.jobResult.title=Measurements frontend.de.iteratec.osm.jobResult.description=The last 150 job runs +frontend.de.iteratec.osm.jobResult.select.label=Job +frontend.de.iteratec.osm.jobResult.select.placeholder=Choose a Job +frontend.de.iteratec.osm.jobResult.select.notFound=No jobs found +frontend.de.iteratec.osm.jobResult.select.loading=Loading... +frontend.de.iteratec.osm.jobResult.select.clearAll=Clear All +frontend.de.iteratec.osm.jobResult.select.search=Type to search frontend.de.iteratec.osm.jobResult.noData=No measurements frontend.de.iteratec.osm.jobResult.tableHead.date=Date frontend.de.iteratec.osm.jobResult.tableHead.testId=Job ID diff --git a/grails-app/i18n/messages_de.properties b/grails-app/i18n/messages_de.properties index a6d2b408c6..0e3c10c288 100644 --- a/grails-app/i18n/messages_de.properties +++ b/grails-app/i18n/messages_de.properties @@ -1289,6 +1289,12 @@ frontend.de.iteratec.osm.distribution.chart.settings.sort.asc=Aufsteigender Medi frontend.de.iteratec.osm.distribution.chart.settings.customerJourney=Customer Journey frontend.de.iteratec.osm.jobResult.title=Messungen frontend.de.iteratec.osm.jobResult.description=Die letzten 150 Jobausführungen +frontend.de.iteratec.osm.jobResult.select.label=Job +frontend.de.iteratec.osm.jobResult.select.placeholder=Wähle einen Job aus +frontend.de.iteratec.osm.jobResult.select.notFound=Keinen Job gefunden +frontend.de.iteratec.osm.jobResult.select.loading=Lädt... +frontend.de.iteratec.osm.jobResult.select.clearAll=Auswahl zurücksetzen +frontend.de.iteratec.osm.jobResult.select.search=Tippen, um zu suchen frontend.de.iteratec.osm.jobResult.noData=Keine Messungen frontend.de.iteratec.osm.jobResult.tableHead.date=Datum frontend.de.iteratec.osm.jobResult.tableHead.testId=Job ID diff --git a/grails-app/services/de/iteratec/osm/measurement/schedule/JobDaoService.groovy b/grails-app/services/de/iteratec/osm/measurement/schedule/JobDaoService.groovy index 0d4305d76d..8da615e946 100644 --- a/grails-app/services/de/iteratec/osm/measurement/schedule/JobDaoService.groovy +++ b/grails-app/services/de/iteratec/osm/measurement/schedule/JobDaoService.groovy @@ -61,7 +61,21 @@ class JobDaoService { /** * Returns a map */ - Map getJobLabels() { + List getJobLabels() { + Job.createCriteria().list { + eq('deleted', false) + order('label') + projections { + property('id') + property('label') + } + } as List + } + + /** + * Returns a map + */ + Map getJobLabelsAsMap() { Job.createCriteria().list { eq('deleted', false) order('label') From 0ae1467e430f6223ff44f110fe03c5a2095e335b Mon Sep 17 00:00:00 2001 From: Daniel Steger Date: Fri, 12 Jun 2020 16:58:25 +0200 Subject: [PATCH 06/14] Handle query params --- .../job-result/job-result.component.html | 35 ++++++++----- .../job-result/job-result.component.scss | 20 ++++++++ .../job-result/job-result.component.ts | 49 +++++++++++++++++-- .../modules/job-result/models/job.model.ts | 5 -- grails-app/i18n/messages.properties | 3 +- grails-app/i18n/messages_de.properties | 9 ++-- 6 files changed, 93 insertions(+), 28 deletions(-) diff --git a/frontend/src/app/modules/job-result/job-result.component.html b/frontend/src/app/modules/job-result/job-result.component.html index 5f08027c3f..767e165f85 100644 --- a/frontend/src/app/modules/job-result/job-result.component.html +++ b/frontend/src/app/modules/job-result/job-result.component.html @@ -6,13 +6,13 @@

-
+
-
- +
{{ 'frontend.de.iteratec.osm.jobResult.noData' | translate }}
@@ -37,41 +38,49 @@

{{ 'frontend.de.iteratec.osm.jobResult.tableHead.testId' | translate }} -

- - - diff --git a/frontend/src/app/modules/job-result/job-result.component.scss b/frontend/src/app/modules/job-result/job-result.component.scss index c010a70fd4..c5e2a6d461 100644 --- a/frontend/src/app/modules/job-result/job-result.component.scss +++ b/frontend/src/app/modules/job-result/job-result.component.scss @@ -2,6 +2,10 @@ margin-left: 10px; } +.job-selection { + margin-bottom: 20px; +} + .sortable { cursor: pointer; } @@ -11,6 +15,22 @@ font-weight: normal; } +.input-filter { + font-weight: normal; + border: none; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0); + height: 28px; + margin-top: 5px; +} + +.input-filter:focus { + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); +} + +.input-filter::placeholder { + font-style: italic; +} + .job-status-error { color: #EB5E55; } diff --git a/frontend/src/app/modules/job-result/job-result.component.ts b/frontend/src/app/modules/job-result/job-result.component.ts index 07d8551129..a52fbc8b9f 100644 --- a/frontend/src/app/modules/job-result/job-result.component.ts +++ b/frontend/src/app/modules/job-result/job-result.component.ts @@ -2,6 +2,7 @@ import {Component, OnInit} from '@angular/core'; import {JobResultDataService} from './services/job-result-data.service'; import {JobResult} from './models/job-result.model'; import {Job} from './models/job.model'; +import {ActivatedRoute, Params, Router} from '@angular/router'; @Component({ selector: 'osm-job-result', @@ -16,7 +17,7 @@ export class JobResultComponent implements OnInit { selectedJob: Job = null; currentSortingRule: { [key: string]: string } = {column: 'date', direction: 'desc'}; - constructor(private dataService: JobResultDataService) { + constructor(private dataService: JobResultDataService, private route: ActivatedRoute, private router: Router) { } ngOnInit() { @@ -25,12 +26,20 @@ export class JobResultComponent implements OnInit { getAllJobs(): void { this.dataService.getAllJobs() - .subscribe((jobs: Job[]) => this.jobs = jobs); + .subscribe((jobs: Job[]) => { + this.jobs = jobs; + this.readQueryParams(); + }); } - getJobResults(jobId: number): void { - this.dataService.getJobResults(jobId) - .subscribe((jobResults: JobResult[]) => this.jobResults = jobResults); + setJob(job: Job): void { + if (job) { + this.writeQueryParams(job.id); + this.getJobResults(job.id); + } else { + this.writeQueryParams(null); + this.jobResults = []; + } } isTestSuccessful(test: JobResult): boolean { @@ -54,6 +63,29 @@ export class JobResultComponent implements OnInit { ); } + private readQueryParams(): void { + this.route.queryParams.subscribe((params: Params) => { + if (this.checkQuery(params)) { + const jobId = parseInt(params.job, 10); + this.selectedJob = this.jobs.find((job: Job) => job.id === jobId); + this.getJobResults(jobId); + } + }); + } + + private writeQueryParams(jobId: number): void { + this.router.navigate([], { + queryParams: { + job: jobId + } + }); + } + + private getJobResults(jobId: number): void { + this.dataService.getJobResults(jobId) + .subscribe((jobResults: JobResult[]) => this.jobResults = jobResults); + } + private compareJobResults(a: JobResult, b: JobResult, column: string, direction: string): number { let directionFactor = 1; if (direction === 'desc') { @@ -77,4 +109,11 @@ export class JobResultComponent implements OnInit { } return 0; } + + private checkQuery(params: Params): boolean { + if (params) { + return !!params.job; + } + return false; + } } diff --git a/frontend/src/app/modules/job-result/models/job.model.ts b/frontend/src/app/modules/job-result/models/job.model.ts index 541acc8cc7..13aa0e4fca 100644 --- a/frontend/src/app/modules/job-result/models/job.model.ts +++ b/frontend/src/app/modules/job-result/models/job.model.ts @@ -1,9 +1,4 @@ export interface Job { id: number; label: string; - script: string; - location: any; - lastRun: Date; - lastChange: Date; - description: string; } diff --git a/grails-app/i18n/messages.properties b/grails-app/i18n/messages.properties index 5620d86e09..57f3f030b0 100644 --- a/grails-app/i18n/messages.properties +++ b/grails-app/i18n/messages.properties @@ -1315,7 +1315,7 @@ frontend.de.iteratec.osm.distribution.chart.settings.sort.asc=Ascending Median frontend.de.iteratec.osm.distribution.chart.settings.customerJourney=Customer Journey frontend.de.iteratec.osm.jobResult.title=Measurements frontend.de.iteratec.osm.jobResult.description=The last 150 job runs -frontend.de.iteratec.osm.jobResult.select.label=Job +frontend.de.iteratec.osm.jobResult.select.label.job=Job frontend.de.iteratec.osm.jobResult.select.placeholder=Choose a Job frontend.de.iteratec.osm.jobResult.select.notFound=No jobs found frontend.de.iteratec.osm.jobResult.select.loading=Loading... @@ -1328,4 +1328,5 @@ frontend.de.iteratec.osm.jobResult.tableHead.testAgent=Test agent frontend.de.iteratec.osm.jobResult.tableHead.jobResultStatus=Status frontend.de.iteratec.osm.jobResult.tableHead.wptStatus=WPT status frontend.de.iteratec.osm.jobResult.tableHead.description=Description +frontend.de.iteratec.osm.jobResult.tableHead.filter=Filter ... frontend.de.iteratec.osm.jobResult.dateFormat=yyyy-MM-dd hh:mm aa diff --git a/grails-app/i18n/messages_de.properties b/grails-app/i18n/messages_de.properties index 0e3c10c288..78729ad8b2 100644 --- a/grails-app/i18n/messages_de.properties +++ b/grails-app/i18n/messages_de.properties @@ -1189,12 +1189,12 @@ frontend.de.iteratec.osm.resultSelection.measurands.add=Messgröße hinzufügen frontend.de.iteratec.osm.resultSelection.pageBrowserConnectivity.label.selectedConnectivities=Anbindung frontend.de.iteratec.osm.resultSelection.pageBrowserConnectivity.label.pageIds=Seite frontend.de.iteratec.osm.resultSelection.pageBrowserConnectivity.label.browserIds=Browser -frontend.de.iteratec.osm.resultSelection.pageBrowserConnectivity.label.locationIds=Location +frontend.de.iteratec.osm.resultSelection.pageBrowserConnectivity.label.locationIds=Standort frontend.de.iteratec.osm.resultSelection.pageBrowserConnectivity.label.measuredEventIds=Messschritt frontend.de.iteratec.osm.resultSelection.pageBrowserConnectivity.parentSelection.selectAll.selectedConnectivities=Alle Anbindungs-Profile auswählen frontend.de.iteratec.osm.resultSelection.pageBrowserConnectivity.parentSelection.selectAll.browserIds=Alle Browser auswählen frontend.de.iteratec.osm.resultSelection.pageBrowserConnectivity.parentSelection.selectAll.pageIds=Alle Seiten auswählen -frontend.de.iteratec.osm.resultSelection.pageBrowserConnectivity.childSelection.selectAll.locationIds=Alle Locations auswählen +frontend.de.iteratec.osm.resultSelection.pageBrowserConnectivity.childSelection.selectAll.locationIds=Alle Standorte auswählen frontend.de.iteratec.osm.resultSelection.pageBrowserConnectivity.childSelection.selectAll.measuredEventIds=Alle Messschritte auswählen frontend.de.iteratec.osm.resultSelection.pageBrowserConnectivity.selectOption.placeholder=Bitte auswählen frontend.de.iteratec.osm.resultSelection.pageBrowserConnectivity.noResults=Keine Ergebnisse im ausgewählten Zeitraum @@ -1277,7 +1277,7 @@ frontend.de.iteratec.chart.datapointSelection.error.multipleServer=Vergleich der frontend.de.iteratec.osm.timeSeries.chart.label.measurand=Messgröße frontend.de.iteratec.osm.timeSeries.chart.label.application=Anwendung frontend.de.iteratec.osm.timeSeries.chart.label.measuredEvent=Messschritt -frontend.de.iteratec.osm.timeSeries.chart.label.location=Location +frontend.de.iteratec.osm.timeSeries.chart.label.location=Standort frontend.de.iteratec.osm.timeSeries.chart.label.connectivity=Anbindung frontend.de.iteratec.osm.timeSeries.chart.label.timestamp=Zeitstempel: frontend.de.iteratec.osm.timeSeries.chart.label.testAgent=Testagent: @@ -1289,7 +1289,7 @@ frontend.de.iteratec.osm.distribution.chart.settings.sort.asc=Aufsteigender Medi frontend.de.iteratec.osm.distribution.chart.settings.customerJourney=Customer Journey frontend.de.iteratec.osm.jobResult.title=Messungen frontend.de.iteratec.osm.jobResult.description=Die letzten 150 Jobausführungen -frontend.de.iteratec.osm.jobResult.select.label=Job +frontend.de.iteratec.osm.jobResult.select.label.job=Job frontend.de.iteratec.osm.jobResult.select.placeholder=Wähle einen Job aus frontend.de.iteratec.osm.jobResult.select.notFound=Keinen Job gefunden frontend.de.iteratec.osm.jobResult.select.loading=Lädt... @@ -1302,4 +1302,5 @@ frontend.de.iteratec.osm.jobResult.tableHead.testAgent=Testagent frontend.de.iteratec.osm.jobResult.tableHead.jobResultStatus=Status frontend.de.iteratec.osm.jobResult.tableHead.wptStatus=WPT-Status frontend.de.iteratec.osm.jobResult.tableHead.description=Beschreibung +frontend.de.iteratec.osm.jobResult.tableHead.filter=Filtern ... frontend.de.iteratec.osm.jobResult.dateFormat=dd.MM.yyyy HH:mm From f9a17964fbf767da6f873810f0d9e75f5b7eb73f Mon Sep 17 00:00:00 2001 From: Daniel Steger Date: Tue, 16 Jun 2020 17:44:15 +0200 Subject: [PATCH 07/14] Filter job results --- .../job-result/job-result.component.html | 64 +++++-- .../job-result/job-result.component.scss | 51 +++++- .../job-result/job-result.component.ts | 173 +++++++++++++++++- .../models/job-result-status.enum.ts | 14 ++ .../job-result/models/status-group.enum.ts | 5 + .../job-result/models/wpt-status.enum.ts | 15 ++ .../osm/result/JobResultController.groovy | 2 +- grails-app/i18n/messages.properties | 8 +- grails-app/i18n/messages_de.properties | 6 +- .../schedule/JobStatisticService.groovy | 8 + 10 files changed, 314 insertions(+), 32 deletions(-) create mode 100644 frontend/src/app/modules/job-result/models/job-result-status.enum.ts create mode 100644 frontend/src/app/modules/job-result/models/status-group.enum.ts create mode 100644 frontend/src/app/modules/job-result/models/wpt-status.enum.ts diff --git a/frontend/src/app/modules/job-result/job-result.component.html b/frontend/src/app/modules/job-result/job-result.component.html index 767e165f85..bb4865cb07 100644 --- a/frontend/src/app/modules/job-result/job-result.component.html +++ b/frontend/src/app/modules/job-result/job-result.component.html @@ -26,17 +26,19 @@

- + + {{ 'frontend.de.iteratec.osm.jobResult.tableHead.testAgent' | translate }} + - + + {{ 'frontend.de.iteratec.osm.jobResult.tableHead.jobResultStatus' | translate }} + - + + {{ 'frontend.de.iteratec.osm.jobResult.tableHead.wptStatus' | translate }} + - + + {{ 'frontend.de.iteratec.osm.jobResult.tableHead.description' | translate }} +
- - - - + - - - + + + + + + + + + +
-
+
+ {{ 'frontend.de.iteratec.osm.jobResult.tableHead.date' | translate }} - -
+ + +
+ {{ 'frontend.de.iteratec.osm.jobResult.tableHead.testId' | translate }} -
+
@@ -46,10 +48,13 @@

- +

+ {{ 'frontend.de.iteratec.osm.jobResult.tableHead.jobResultStatus' | translate }} - + + + {{ 'frontend.de.iteratec.osm.jobResult.tableHead.wptStatus' | translate }} - + + @@ -79,15 +96,19 @@

-

-
{{ test.date | date:('frontend.de.iteratec.osm.jobResult.dateFormat' | translate) }}
@@ -96,9 +117,16 @@

{{ test.testAgent ? test.testAgent : '-' }}{{ test.jobResultStatus }}{{ test.wptStatus }}{{ test.description }}{{ test.jobResultStatus ? test.jobResultStatus : '-' }}{{ test.wptStatus ? test.wptStatus : '-' }}{{ test.description ? test.description : '-' }}
-----
diff --git a/frontend/src/app/modules/job-result/job-result.component.scss b/frontend/src/app/modules/job-result/job-result.component.scss index c5e2a6d461..9a1f407da1 100644 --- a/frontend/src/app/modules/job-result/job-result.component.scss +++ b/frontend/src/app/modules/job-result/job-result.component.scss @@ -10,27 +10,68 @@ cursor: pointer; } -.job-id-table-head { +.date-table-head { + padding-bottom: 23px; +} + +.job-id { font-style: italic; font-weight: normal; + margin-bottom: 5px; +} + +.date-filter-icon { + margin-left: 6px; + color: #999999; + opacity: 0.8; } .input-filter { font-weight: normal; border: none; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0); - height: 28px; + height: 30px; margin-top: 5px; + margin-left: -12px; +} + +.input-filter:hover { + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.075); } .input-filter:focus { - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(0, 0, 0, 0.075); } .input-filter::placeholder { font-style: italic; } +.status-table-head { + max-width: 15em; +} + +.ng-select.custom ::ng-deep .ng-select-container { + font-weight: normal; + border: none; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0); + min-height: 0; + margin-top: 5px; + margin-left: -10px; +} + +.ng-select.custom ::ng-deep .ng-option { + font-weight: normal; +} + +.ng-select.custom ::ng-deep .ng-select-container:hover { + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.075); +} + +.ng-select.custom ::ng-deep .ng-select-container .ng-placeholder { + font-style: italic; +} + .job-status-error { color: #EB5E55; } @@ -42,3 +83,7 @@ .job-id { font-style: italic; } + +.no-data-after-filter { + min-width: 20%; +} diff --git a/frontend/src/app/modules/job-result/job-result.component.ts b/frontend/src/app/modules/job-result/job-result.component.ts index a52fbc8b9f..5898f99bae 100644 --- a/frontend/src/app/modules/job-result/job-result.component.ts +++ b/frontend/src/app/modules/job-result/job-result.component.ts @@ -3,6 +3,10 @@ import {JobResultDataService} from './services/job-result-data.service'; import {JobResult} from './models/job-result.model'; import {Job} from './models/job.model'; import {ActivatedRoute, Params, Router} from '@angular/router'; +import {TranslateService} from '@ngx-translate/core'; +import {StatusGroup} from './models/status-group.enum'; +import {JobResultStatus} from './models/job-result-status.enum'; +import {WptStatus} from './models/wpt-status.enum'; @Component({ selector: 'osm-job-result', @@ -13,13 +17,26 @@ export class JobResultComponent implements OnInit { jobs: Job[] = []; jobResults: JobResult[] = []; + allJobResultStatus: string[] = Object.values(JobResultStatus); + allWptStatus: string[] = Object.values(WptStatus); + filteredJobResults: JobResult[] = []; selectedJob: Job = null; currentSortingRule: { [key: string]: string } = {column: 'date', direction: 'desc'}; + filter: { [key: string]: any } = {testAgent: '', jobResultStatus: [], wptStatus: [], description: ''}; - constructor(private dataService: JobResultDataService, private route: ActivatedRoute, private router: Router) { + constructor(private dataService: JobResultDataService, + private route: ActivatedRoute, + private router: Router, + private translateService: TranslateService) { } + jobResultStatusGroupByFn = (test: string): string => this.getJobResultStatusGroupName(test); + + wptStatusGroupByFn = (test: string): string => this.getWptStatusGroupName(test); + + groupValueFn = (groupName: string, children: any[]): any[] => children; + ngOnInit() { this.getAllJobs(); } @@ -39,13 +56,10 @@ export class JobResultComponent implements OnInit { } else { this.writeQueryParams(null); this.jobResults = []; + this.filteredJobResults = []; } } - isTestSuccessful(test: JobResult): boolean { - return test.jobResultStatus === 'Finished' && test.wptStatus === 'COMPLETED (200)' && test.description === 'Ok'; - } - sort(column: string) { if (column === this.currentSortingRule.column) { if (this.currentSortingRule.direction === 'desc') { @@ -58,11 +72,74 @@ export class JobResultComponent implements OnInit { this.currentSortingRule.direction = 'asc'; } - this.jobResults.sort((a: JobResult, b: JobResult) => + this.filteredJobResults.sort((a: JobResult, b: JobResult) => this.compareJobResults(a, b, this.currentSortingRule.column, this.currentSortingRule.direction) ); } + setFilter(): void { + this.filteredJobResults = this.jobResults.filter((jobResult: JobResult) => { + if (this.arePropertiesThatAreFilteredEmpty(jobResult)) { + return false; + } + return this.isJobResultIncludingTerms(jobResult); + }); + } + + isTestNotTerminated(jobResultStatus: string): boolean { + const notTerminatedStatus: string[] = [ + JobResultStatus.WAITING, + JobResultStatus.RUNNING + ]; + return notTerminatedStatus.includes(jobResultStatus); + } + + isTestSuccessful(jobResultStatus: string): boolean { + const successfulStatus: string[] = [ + JobResultStatus.SUCCESS + ]; + return successfulStatus.includes(jobResultStatus); + } + + hasTestFailed(jobResultStatus: string): boolean { + const failedStatus: string[] = [ + JobResultStatus.INCOMPLETE, + JobResultStatus.LAUNCH_ERROR, + JobResultStatus.FETCH_ERROR, + JobResultStatus.PERSISTENCE_ERROR, + JobResultStatus.TIMEOUT, + JobResultStatus.FAILED, + JobResultStatus.CANCELED, + JobResultStatus.ORPHANED, + JobResultStatus.DID_NOT_START + ]; + return failedStatus.includes(jobResultStatus); + } + + private getJobResultStatusGroupName(test: string): string { + if (this.isTestNotTerminated(test)) { + return this.translateService.instant(StatusGroup.NOT_TERMINATED); + } + if (this.isTestSuccessful(test)) { + return this.translateService.instant(StatusGroup.SUCCESS); + } + if (this.hasTestFailed(test)) { + return this.translateService.instant(StatusGroup.FAILED); + } + } + + private getWptStatusGroupName(test: string): string { + if (this.isWptNotTerminated(test)) { + return this.translateService.instant(StatusGroup.NOT_TERMINATED); + } + if (this.isWptSuccessful(test)) { + return this.translateService.instant(StatusGroup.SUCCESS); + } + if (this.hasWptFailed(test)) { + return this.translateService.instant(StatusGroup.FAILED); + } + } + private readQueryParams(): void { this.route.queryParams.subscribe((params: Params) => { if (this.checkQuery(params)) { @@ -83,7 +160,10 @@ export class JobResultComponent implements OnInit { private getJobResults(jobId: number): void { this.dataService.getJobResults(jobId) - .subscribe((jobResults: JobResult[]) => this.jobResults = jobResults); + .subscribe((jobResults: JobResult[]) => { + this.jobResults = jobResults; + this.filteredJobResults = jobResults; + }); } private compareJobResults(a: JobResult, b: JobResult, column: string, direction: string): number { @@ -116,4 +196,83 @@ export class JobResultComponent implements OnInit { } return false; } + + private arePropertiesThatAreFilteredEmpty(jobResult: JobResult): boolean { + if (!jobResult.testAgent && this.filter.testAgent) { + return true; + } + if (!jobResult.jobResultStatus && this.filter.jobResultStatus.length > 0) { + return true; + } + if (!jobResult.wptStatus && this.filter.wptStatus.length > 0) { + return true; + } + if (!jobResult.description && this.filter.description) { + return true; + } + return false; + } + + private isJobResultIncludingTerms(jobResult: JobResult): boolean { + if (jobResult.testAgent && !jobResult.testAgent.toLowerCase().includes(this.filter.testAgent.toLowerCase())) { + return false; + } + if (jobResult.jobResultStatus && this.filter.jobResultStatus.length > 0) { + if (!this.isStatusIncludingTerms(jobResult.jobResultStatus, this.filter.jobResultStatus)) { + return false; + } + } + if (jobResult.wptStatus && this.filter.wptStatus.length > 0) { + if (!this.isStatusIncludingTerms(jobResult.wptStatus, this.filter.wptStatus)) { + return false; + } + } + if (jobResult.description && !jobResult.description.toLowerCase().includes(this.filter.description.toLowerCase())) { + return false; + } + return true; + } + + private isStatusIncludingTerms(status: string, terms: any): boolean { + return terms.find((termOrTermList: (string | string[])) => { + if (Array.isArray(termOrTermList)) { + return termOrTermList.find((term: string) => status.toLowerCase().includes(term.toLowerCase())); + } + if (typeof termOrTermList === 'string') { + return status.toLowerCase().includes(termOrTermList.toLowerCase()); + } + return false; + }); + } + + private isWptNotTerminated(wptStatus: string): boolean { + const notTerminatedStatus: string[] = [ + WptStatus.UNKNOWN, + WptStatus.PENDING, + WptStatus.IN_PROGRESS + ]; + return notTerminatedStatus.includes(wptStatus); + } + + private isWptSuccessful(wptStatus: string): boolean { + const successfulStatus: string[] = [ + WptStatus.SUCCESSFUL, + WptStatus.COMPLETED, + WptStatus.TEST_HAS_A_UNDEFINED_PROBLEM, + WptStatus.TEST_COMPLETED_BUT_INDIVIDUAL_REQUEST_FAILED + ]; + return successfulStatus.includes(wptStatus); + } + + private hasWptFailed(wptStatus: string): boolean { + const failedStatus: string[] = [ + WptStatus.TESTED_APPLICATION_CLIENT_ERROR, + WptStatus.TESTED_APPLICATION_INTERNAL_SERVER_ERROR, + WptStatus.TEST_DID_NOT_START, + WptStatus.TEST_FAILED_WAITING_FOR_DOM_ELEMENT, + WptStatus.TEST_TIMED_OUT, + WptStatus.TEST_TIMED_OUT_CONTENT_ERRORS + ]; + return failedStatus.includes(wptStatus); + } } diff --git a/frontend/src/app/modules/job-result/models/job-result-status.enum.ts b/frontend/src/app/modules/job-result/models/job-result-status.enum.ts new file mode 100644 index 0000000000..5cb0860a9f --- /dev/null +++ b/frontend/src/app/modules/job-result/models/job-result-status.enum.ts @@ -0,0 +1,14 @@ +export enum JobResultStatus { + WAITING = 'Waiting', + RUNNING = 'Running', + SUCCESS = 'Finished', + INCOMPLETE = 'Incomplete', + LAUNCH_ERROR = 'Failed to start', + FETCH_ERROR = 'Failed to fetch result', + PERSISTENCE_ERROR = 'Failed to save result', + TIMEOUT = 'Timed out', + FAILED = 'Failed', + CANCELED = 'Canceled', + ORPHANED = 'Orphaned', + DID_NOT_START = 'Did not start' +} diff --git a/frontend/src/app/modules/job-result/models/status-group.enum.ts b/frontend/src/app/modules/job-result/models/status-group.enum.ts new file mode 100644 index 0000000000..de348a7e37 --- /dev/null +++ b/frontend/src/app/modules/job-result/models/status-group.enum.ts @@ -0,0 +1,5 @@ +export enum StatusGroup { + NOT_TERMINATED = 'frontend.de.iteratec.osm.jobResult.filter.status.group.notTerminated', + SUCCESS = 'frontend.de.iteratec.osm.jobResult.filter.status.group.success', + FAILED = 'frontend.de.iteratec.osm.jobResult.filter.status.group.failed' +} diff --git a/frontend/src/app/modules/job-result/models/wpt-status.enum.ts b/frontend/src/app/modules/job-result/models/wpt-status.enum.ts new file mode 100644 index 0000000000..156a97cdb5 --- /dev/null +++ b/frontend/src/app/modules/job-result/models/wpt-status.enum.ts @@ -0,0 +1,15 @@ +export enum WptStatus { + UNKNOWN = 'UNKNOWN (-1)', + SUCCESSFUL = 'SUCCESSFUL (0)', + PENDING = 'PENDING (100)', + IN_PROGRESS = 'IN_PROGRESS (101)', + COMPLETED = 'COMPLETED (200)', + TESTED_APPLICATION_CLIENT_ERROR = 'TESTED_APPLICATION_CLIENTERROR (400 - 499)', + TESTED_APPLICATION_INTERNAL_SERVER_ERROR = 'TESTED_APPLICATION_INTERNALSERVERERROR (500 - 599)', + TEST_DID_NOT_START = 'TEST_DID_NOT_START (701)', + TEST_HAS_A_UNDEFINED_PROBLEM = 'TEST_HAS_A_UNDEFINED_PROBLEM (12999)', + TEST_FAILED_WAITING_FOR_DOM_ELEMENT = 'TEST_FAILED_WAITING_FOR_DOM_ELEMENT (99996)', + TEST_TIMED_OUT = 'TEST_TIMED_OUT (99997)', + TEST_TIMED_OUT_CONTENT_ERRORS = 'TEST_TIMED_OUT_CONTENT_ERRORS (99998)', + TEST_COMPLETED_BUT_INDIVIDUAL_REQUEST_FAILED = 'TEST_COMPLETED_BUT_INDIVIDUAL_REQUEST_FAILED (99999)' +} diff --git a/grails-app/controllers/de/iteratec/osm/result/JobResultController.groovy b/grails-app/controllers/de/iteratec/osm/result/JobResultController.groovy index c3f54de6ec..fe47f141f1 100644 --- a/grails-app/controllers/de/iteratec/osm/result/JobResultController.groovy +++ b/grails-app/controllers/de/iteratec/osm/result/JobResultController.groovy @@ -36,7 +36,7 @@ class JobResultController { List jobResultsForJob Job job = Job.get(jobId) if (job) { - jobResultsForJob = jobStatisticService.getLast150CompletedJobResultsFor(job) + jobResultsForJob = jobStatisticService.getLast150JobResultsFor(job) } List dtos = [] diff --git a/grails-app/i18n/messages.properties b/grails-app/i18n/messages.properties index 57f3f030b0..670b8890c6 100644 --- a/grails-app/i18n/messages.properties +++ b/grails-app/i18n/messages.properties @@ -1328,5 +1328,9 @@ frontend.de.iteratec.osm.jobResult.tableHead.testAgent=Test agent frontend.de.iteratec.osm.jobResult.tableHead.jobResultStatus=Status frontend.de.iteratec.osm.jobResult.tableHead.wptStatus=WPT status frontend.de.iteratec.osm.jobResult.tableHead.description=Description -frontend.de.iteratec.osm.jobResult.tableHead.filter=Filter ... -frontend.de.iteratec.osm.jobResult.dateFormat=yyyy-MM-dd hh:mm aa +frontend.de.iteratec.osm.jobResult.filter.placeholder=Filter +frontend.de.iteratec.osm.jobResult.filter.notFound=- +frontend.de.iteratec.osm.jobResult.filter.status.group.notTerminated=Group: Not terminated +frontend.de.iteratec.osm.jobResult.filter.status.group.success=Group: Success +frontend.de.iteratec.osm.jobResult.filter.status.group.failed=Group: Failed +frontend.de.iteratec.osm.jobResult.dateFormat=yyyy-MM-dd HH:mm diff --git a/grails-app/i18n/messages_de.properties b/grails-app/i18n/messages_de.properties index 78729ad8b2..41a14750ea 100644 --- a/grails-app/i18n/messages_de.properties +++ b/grails-app/i18n/messages_de.properties @@ -1302,5 +1302,9 @@ frontend.de.iteratec.osm.jobResult.tableHead.testAgent=Testagent frontend.de.iteratec.osm.jobResult.tableHead.jobResultStatus=Status frontend.de.iteratec.osm.jobResult.tableHead.wptStatus=WPT-Status frontend.de.iteratec.osm.jobResult.tableHead.description=Beschreibung -frontend.de.iteratec.osm.jobResult.tableHead.filter=Filtern ... +frontend.de.iteratec.osm.jobResult.filter.placeholder=Filtern +frontend.de.iteratec.osm.jobResult.filter.notFound=- +frontend.de.iteratec.osm.jobResult.filter.status.group.notTerminated=Gruppe: Nicht beendet +frontend.de.iteratec.osm.jobResult.filter.status.group.success=Gruppe: Erfolgreich +frontend.de.iteratec.osm.jobResult.filter.status.group.failed=Gruppe: Fehlgeschlagen frontend.de.iteratec.osm.jobResult.dateFormat=dd.MM.yyyy HH:mm diff --git a/grails-app/services/de/iteratec/osm/measurement/schedule/JobStatisticService.groovy b/grails-app/services/de/iteratec/osm/measurement/schedule/JobStatisticService.groovy index 33b16bc697..96b6030859 100644 --- a/grails-app/services/de/iteratec/osm/measurement/schedule/JobStatisticService.groovy +++ b/grails-app/services/de/iteratec/osm/measurement/schedule/JobStatisticService.groovy @@ -55,4 +55,12 @@ class JobStatisticService { maxResults(150) } } + + List getLast150JobResultsFor(Job job) { + return JobResult.createCriteria().list{ + eq("job", job) + order("date", "desc") + maxResults(150) + } + } } From 5b143b8981ddc0b56a8a6b287abfafba4b5b56ae Mon Sep 17 00:00:00 2001 From: Daniel Steger Date: Wed, 17 Jun 2020 17:18:33 +0200 Subject: [PATCH 08/14] Filter job results and write/read url params --- .../job-result/job-result.component.ts | 218 ++++++++++++------ .../models/job-result-filter.model.ts | 8 + .../job-result/models/status-group.enum.ts | 6 +- grails-app/i18n/messages.properties | 2 +- grails-app/i18n/messages_de.properties | 2 +- 5 files changed, 158 insertions(+), 78 deletions(-) create mode 100644 frontend/src/app/modules/job-result/models/job-result-filter.model.ts diff --git a/frontend/src/app/modules/job-result/job-result.component.ts b/frontend/src/app/modules/job-result/job-result.component.ts index 5898f99bae..55af819136 100644 --- a/frontend/src/app/modules/job-result/job-result.component.ts +++ b/frontend/src/app/modules/job-result/job-result.component.ts @@ -7,6 +7,7 @@ import {TranslateService} from '@ngx-translate/core'; import {StatusGroup} from './models/status-group.enum'; import {JobResultStatus} from './models/job-result-status.enum'; import {WptStatus} from './models/wpt-status.enum'; +import {JobResultFilter} from './models/job-result-filter.model'; @Component({ selector: 'osm-job-result', @@ -23,7 +24,50 @@ export class JobResultComponent implements OnInit { filteredJobResults: JobResult[] = []; selectedJob: Job = null; currentSortingRule: { [key: string]: string } = {column: 'date', direction: 'desc'}; - filter: { [key: string]: any } = {testAgent: '', jobResultStatus: [], wptStatus: [], description: ''}; + filter: JobResultFilter = {from: null, to: null, testAgent: '', jobResultStatus: [], wptStatus: [], description: ''}; + + JOB_RESULT_STATUS_GROUPS: {[key: string]: string []} = { + GROUP_NOT_TERMINATED: [ + JobResultStatus.WAITING, + JobResultStatus.RUNNING + ], + GROUP_SUCCESS: [ + JobResultStatus.SUCCESS + ], + GROUP_FAILED: [ + JobResultStatus.INCOMPLETE, + JobResultStatus.LAUNCH_ERROR, + JobResultStatus.FETCH_ERROR, + JobResultStatus.PERSISTENCE_ERROR, + JobResultStatus.TIMEOUT, + JobResultStatus.FAILED, + JobResultStatus.CANCELED, + JobResultStatus.ORPHANED, + JobResultStatus.DID_NOT_START + ] + }; + + WPT_STATUS_GROUPS: {[key: string]: string []} = { + GROUP_NOT_TERMINATED: [ + WptStatus.UNKNOWN, + WptStatus.PENDING, + WptStatus.IN_PROGRESS + ], + GROUP_SUCCESS: [ + WptStatus.SUCCESSFUL, + WptStatus.COMPLETED, + WptStatus.TEST_HAS_A_UNDEFINED_PROBLEM, + WptStatus.TEST_COMPLETED_BUT_INDIVIDUAL_REQUEST_FAILED + ], + GROUP_FAILED: [ + WptStatus.TESTED_APPLICATION_CLIENT_ERROR, + WptStatus.TESTED_APPLICATION_INTERNAL_SERVER_ERROR, + WptStatus.TEST_DID_NOT_START, + WptStatus.TEST_FAILED_WAITING_FOR_DOM_ELEMENT, + WptStatus.TEST_TIMED_OUT, + WptStatus.TEST_TIMED_OUT_CONTENT_ERRORS + ] + }; constructor(private dataService: JobResultDataService, private route: ActivatedRoute, @@ -31,11 +75,11 @@ export class JobResultComponent implements OnInit { private translateService: TranslateService) { } - jobResultStatusGroupByFn = (test: string): string => this.getJobResultStatusGroupName(test); + jobResultStatusGroupByFn = (jobResultStatus: string): string => this.getJobResultStatusGroupName(jobResultStatus); - wptStatusGroupByFn = (test: string): string => this.getWptStatusGroupName(test); + wptStatusGroupByFn = (wptStatus: string): string => this.getWptStatusGroupName(wptStatus); - groupValueFn = (groupName: string, children: any[]): any[] => children; + groupValueFn = (groupName: string, children: any[]): any => ({label: groupName, children: children}); ngOnInit() { this.getAllJobs(); @@ -51,10 +95,10 @@ export class JobResultComponent implements OnInit { setJob(job: Job): void { if (job) { - this.writeQueryParams(job.id); + this.writeQueryParams(); this.getJobResults(job.id); } else { - this.writeQueryParams(null); + this.writeQueryParams(); this.jobResults = []; this.filteredJobResults = []; } @@ -78,6 +122,7 @@ export class JobResultComponent implements OnInit { } setFilter(): void { + this.writeQueryParams(); this.filteredJobResults = this.jobResults.filter((jobResult: JobResult) => { if (this.arePropertiesThatAreFilteredEmpty(jobResult)) { return false; @@ -87,56 +132,38 @@ export class JobResultComponent implements OnInit { } isTestNotTerminated(jobResultStatus: string): boolean { - const notTerminatedStatus: string[] = [ - JobResultStatus.WAITING, - JobResultStatus.RUNNING - ]; - return notTerminatedStatus.includes(jobResultStatus); + return this.JOB_RESULT_STATUS_GROUPS.GROUP_NOT_TERMINATED.includes(jobResultStatus); } isTestSuccessful(jobResultStatus: string): boolean { - const successfulStatus: string[] = [ - JobResultStatus.SUCCESS - ]; - return successfulStatus.includes(jobResultStatus); + return this.JOB_RESULT_STATUS_GROUPS.GROUP_SUCCESS.includes(jobResultStatus); } hasTestFailed(jobResultStatus: string): boolean { - const failedStatus: string[] = [ - JobResultStatus.INCOMPLETE, - JobResultStatus.LAUNCH_ERROR, - JobResultStatus.FETCH_ERROR, - JobResultStatus.PERSISTENCE_ERROR, - JobResultStatus.TIMEOUT, - JobResultStatus.FAILED, - JobResultStatus.CANCELED, - JobResultStatus.ORPHANED, - JobResultStatus.DID_NOT_START - ]; - return failedStatus.includes(jobResultStatus); + return this.JOB_RESULT_STATUS_GROUPS.GROUP_FAILED.includes(jobResultStatus); } - private getJobResultStatusGroupName(test: string): string { - if (this.isTestNotTerminated(test)) { - return this.translateService.instant(StatusGroup.NOT_TERMINATED); + private getJobResultStatusGroupName(jobResultStatus: string): string { + if (this.isTestNotTerminated(jobResultStatus)) { + return this.translateService.instant(StatusGroup.GROUP_NOT_TERMINATED); } - if (this.isTestSuccessful(test)) { - return this.translateService.instant(StatusGroup.SUCCESS); + if (this.isTestSuccessful(jobResultStatus)) { + return this.translateService.instant(StatusGroup.GROUP_SUCCESS); } - if (this.hasTestFailed(test)) { - return this.translateService.instant(StatusGroup.FAILED); + if (this.hasTestFailed(jobResultStatus)) { + return this.translateService.instant(StatusGroup.GROUP_FAILED); } } - private getWptStatusGroupName(test: string): string { - if (this.isWptNotTerminated(test)) { - return this.translateService.instant(StatusGroup.NOT_TERMINATED); + private getWptStatusGroupName(wptStatus: string): string { + if (this.isWptNotTerminated(wptStatus)) { + return this.translateService.instant(StatusGroup.GROUP_NOT_TERMINATED); } - if (this.isWptSuccessful(test)) { - return this.translateService.instant(StatusGroup.SUCCESS); + if (this.isWptSuccessful(wptStatus)) { + return this.translateService.instant(StatusGroup.GROUP_SUCCESS); } - if (this.hasWptFailed(test)) { - return this.translateService.instant(StatusGroup.FAILED); + if (this.hasWptFailed(wptStatus)) { + return this.translateService.instant(StatusGroup.GROUP_FAILED); } } @@ -144,17 +171,34 @@ export class JobResultComponent implements OnInit { this.route.queryParams.subscribe((params: Params) => { if (this.checkQuery(params)) { const jobId = parseInt(params.job, 10); - this.selectedJob = this.jobs.find((job: Job) => job.id === jobId); - this.getJobResults(jobId); + if (jobId) { + this.selectedJob = this.jobs.find((job: Job) => job.id === jobId); + // this.filter.from = decodeURIComponent(params.from); + // this.filter.to = decodeURIComponent(params.to); + this.filter.testAgent = params.testAgent ? decodeURIComponent(params.testAgent) : ''; + this.filter.jobResultStatus = this.readStatusByQueryParam(params.status, JobResultStatus, this.JOB_RESULT_STATUS_GROUPS); + this.filter.wptStatus = this.readStatusByQueryParam(params.wptStatus, WptStatus, this.WPT_STATUS_GROUPS); + this.filter.description = params.description ? decodeURIComponent(params.description) : ''; + this.getJobResults(jobId); + } } - }); + }).unsubscribe(); } - private writeQueryParams(jobId: number): void { + private writeQueryParams(): void { + const params: Params = { + job: this.selectedJob.id, + // from: encodeURIComponent(this.filter.from), + // to: encodeURIComponent(this.filter.to), + testAgent: this.filter.testAgent !== '' ? encodeURIComponent(this.filter.testAgent) : null, + status: this.writeStatusAsQueryParam(this.filter.jobResultStatus, JobResultStatus), + wptStatus: this.writeStatusAsQueryParam(this.filter.wptStatus, WptStatus), + description: this.filter.description !== '' ? encodeURIComponent(this.filter.description) : null + }; + this.router.navigate([], { - queryParams: { - job: jobId - } + queryParams: params, + replaceUrl: true }); } @@ -163,6 +207,7 @@ export class JobResultComponent implements OnInit { .subscribe((jobResults: JobResult[]) => { this.jobResults = jobResults; this.filteredJobResults = jobResults; + this.setFilter(); }); } @@ -235,44 +280,71 @@ export class JobResultComponent implements OnInit { private isStatusIncludingTerms(status: string, terms: any): boolean { return terms.find((termOrTermList: (string | string[])) => { - if (Array.isArray(termOrTermList)) { - return termOrTermList.find((term: string) => status.toLowerCase().includes(term.toLowerCase())); - } if (typeof termOrTermList === 'string') { return status.toLowerCase().includes(termOrTermList.toLowerCase()); } + if (typeof termOrTermList === 'object' && termOrTermList['children'] && Array.isArray(termOrTermList['children'])) { + return termOrTermList['children'].find((term: string) => status.toLowerCase().includes(term.toLowerCase())); + } return false; }); } private isWptNotTerminated(wptStatus: string): boolean { - const notTerminatedStatus: string[] = [ - WptStatus.UNKNOWN, - WptStatus.PENDING, - WptStatus.IN_PROGRESS - ]; - return notTerminatedStatus.includes(wptStatus); + return this.WPT_STATUS_GROUPS.GROUP_NOT_TERMINATED.includes(wptStatus); } private isWptSuccessful(wptStatus: string): boolean { - const successfulStatus: string[] = [ - WptStatus.SUCCESSFUL, - WptStatus.COMPLETED, - WptStatus.TEST_HAS_A_UNDEFINED_PROBLEM, - WptStatus.TEST_COMPLETED_BUT_INDIVIDUAL_REQUEST_FAILED - ]; - return successfulStatus.includes(wptStatus); + return this.WPT_STATUS_GROUPS.GROUP_SUCCESS.includes(wptStatus); } private hasWptFailed(wptStatus: string): boolean { - const failedStatus: string[] = [ - WptStatus.TESTED_APPLICATION_CLIENT_ERROR, - WptStatus.TESTED_APPLICATION_INTERNAL_SERVER_ERROR, - WptStatus.TEST_DID_NOT_START, - WptStatus.TEST_FAILED_WAITING_FOR_DOM_ELEMENT, - WptStatus.TEST_TIMED_OUT, - WptStatus.TEST_TIMED_OUT_CONTENT_ERRORS - ]; - return failedStatus.includes(wptStatus); + return this.WPT_STATUS_GROUPS.GROUP_FAILED.includes(wptStatus); + } + + private writeStatusAsQueryParam(statusList: (string | object)[], enumeration: any): string[] { + if (statusList.length > 0) { + return statusList.map(status => { + if (typeof status === 'string') { + return Object.keys(enumeration).find(key => enumeration[key] === status).toLowerCase(); + } + if (typeof status === 'object' && status['label'] && typeof status['label'] === 'string') { + return Object.keys(StatusGroup).find(key => + this.translateService.instant(StatusGroup[key]) === status['label']).toLowerCase(); + } + }); + } + return null; + } + + private readStatusByQueryParam(statusParam: (string | string[]), + enumeration: any, + statusGroupType: {[key: string]: string []} + ): (string | object)[] { + if (!statusParam) { + return []; + } + if (typeof statusParam === 'string') { + statusParam = statusParam.toUpperCase(); + if (Object.keys(StatusGroup).find(key => key === statusParam)) { + return [].concat({ + label: this.translateService.instant(StatusGroup[statusParam]), + children: statusGroupType[statusParam] + }); + } + return [].concat(enumeration[statusParam]); + } + if (Array.isArray(statusParam)) { + return statusParam.map((status: string) => { + status = status.toUpperCase(); + if (Object.keys(StatusGroup).find(key => key === status)) { + return { + label: this.translateService.instant(StatusGroup[status]), + children: statusGroupType[status] + }; + } + return enumeration[status]; + }); + } } } diff --git a/frontend/src/app/modules/job-result/models/job-result-filter.model.ts b/frontend/src/app/modules/job-result/models/job-result-filter.model.ts new file mode 100644 index 0000000000..7ebdac8764 --- /dev/null +++ b/frontend/src/app/modules/job-result/models/job-result-filter.model.ts @@ -0,0 +1,8 @@ +export interface JobResultFilter { + from: Date; + to: Date; + testAgent: string; + jobResultStatus: (string | object)[]; + wptStatus: (string | object)[]; + description: string; +} diff --git a/frontend/src/app/modules/job-result/models/status-group.enum.ts b/frontend/src/app/modules/job-result/models/status-group.enum.ts index de348a7e37..5faa441324 100644 --- a/frontend/src/app/modules/job-result/models/status-group.enum.ts +++ b/frontend/src/app/modules/job-result/models/status-group.enum.ts @@ -1,5 +1,5 @@ export enum StatusGroup { - NOT_TERMINATED = 'frontend.de.iteratec.osm.jobResult.filter.status.group.notTerminated', - SUCCESS = 'frontend.de.iteratec.osm.jobResult.filter.status.group.success', - FAILED = 'frontend.de.iteratec.osm.jobResult.filter.status.group.failed' + GROUP_NOT_TERMINATED = 'frontend.de.iteratec.osm.jobResult.filter.status.group.notTerminated', + GROUP_SUCCESS = 'frontend.de.iteratec.osm.jobResult.filter.status.group.success', + GROUP_FAILED = 'frontend.de.iteratec.osm.jobResult.filter.status.group.failed' } diff --git a/grails-app/i18n/messages.properties b/grails-app/i18n/messages.properties index 670b8890c6..d22b0f58d5 100644 --- a/grails-app/i18n/messages.properties +++ b/grails-app/i18n/messages.properties @@ -1323,7 +1323,7 @@ frontend.de.iteratec.osm.jobResult.select.clearAll=Clear All frontend.de.iteratec.osm.jobResult.select.search=Type to search frontend.de.iteratec.osm.jobResult.noData=No measurements frontend.de.iteratec.osm.jobResult.tableHead.date=Date -frontend.de.iteratec.osm.jobResult.tableHead.testId=Job ID +frontend.de.iteratec.osm.jobResult.tableHead.testId=Test ID frontend.de.iteratec.osm.jobResult.tableHead.testAgent=Test agent frontend.de.iteratec.osm.jobResult.tableHead.jobResultStatus=Status frontend.de.iteratec.osm.jobResult.tableHead.wptStatus=WPT status diff --git a/grails-app/i18n/messages_de.properties b/grails-app/i18n/messages_de.properties index 41a14750ea..8d5f824e60 100644 --- a/grails-app/i18n/messages_de.properties +++ b/grails-app/i18n/messages_de.properties @@ -1297,7 +1297,7 @@ frontend.de.iteratec.osm.jobResult.select.clearAll=Auswahl zurücksetzen frontend.de.iteratec.osm.jobResult.select.search=Tippen, um zu suchen frontend.de.iteratec.osm.jobResult.noData=Keine Messungen frontend.de.iteratec.osm.jobResult.tableHead.date=Datum -frontend.de.iteratec.osm.jobResult.tableHead.testId=Job ID +frontend.de.iteratec.osm.jobResult.tableHead.testId=Test ID frontend.de.iteratec.osm.jobResult.tableHead.testAgent=Testagent frontend.de.iteratec.osm.jobResult.tableHead.jobResultStatus=Status frontend.de.iteratec.osm.jobResult.tableHead.wptStatus=WPT-Status From 63f45e047cdea65e968a8d08bc58decfc1845d32 Mon Sep 17 00:00:00 2001 From: Daniel Steger Date: Thu, 18 Jun 2020 12:46:13 +0200 Subject: [PATCH 09/14] Added clearing filter. Refactored status. --- .../job-result/job-result.component.html | 55 ++++-- .../job-result/job-result.component.scss | 43 ++++- .../job-result/job-result.component.ts | 181 ++---------------- .../services/status.service.spec.ts | 12 ++ .../job-result/services/status.service.ts | 162 ++++++++++++++++ 5 files changed, 266 insertions(+), 187 deletions(-) create mode 100644 frontend/src/app/modules/job-result/services/status.service.spec.ts create mode 100644 frontend/src/app/modules/job-result/services/status.service.ts diff --git a/frontend/src/app/modules/job-result/job-result.component.html b/frontend/src/app/modules/job-result/job-result.component.html index bb4865cb07..39046c268f 100644 --- a/frontend/src/app/modules/job-result/job-result.component.html +++ b/frontend/src/app/modules/job-result/job-result.component.html @@ -26,6 +26,7 @@

+ - - - - + - - + diff --git a/frontend/src/app/modules/job-result/job-result.component.scss b/frontend/src/app/modules/job-result/job-result.component.scss index 42bef9bdc0..35525a5697 100644 --- a/frontend/src/app/modules/job-result/job-result.component.scss +++ b/frontend/src/app/modules/job-result/job-result.component.scss @@ -6,10 +6,6 @@ margin-bottom: 20px; } -.date-table-head { - padding-bottom: 23px; -} - .table-head { max-width: 15em; } @@ -18,20 +14,14 @@ cursor: pointer; } -.job-id { - font-style: italic; - font-weight: normal; - margin-bottom: 5px; -} - -.date-filter-icon { - margin-left: 6px; - color: #999999; - opacity: 0.8; +.date-input-container { + display: inline-flex; + width: 100%; } -.input-container { - // display: inline; +.date-input-wrapper { + width: 100%; + margin-right: 10px; } .input-filter { @@ -44,14 +34,10 @@ margin-left: -12px; } -.input-container:hover .input-filter { +.input-filter:focus, .input-from:hover, .input-to:hover, .input-container:hover .input-filter { box-shadow: 0 1px 0 rgba(0, 0, 0, 0.075); } -.input-filter:focus, .input-container:hover .input-filter:focus { - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(0, 0, 0, 0.075); -} - .input-filter::placeholder { font-style: italic; } @@ -78,7 +64,41 @@ color: #D0021B } -.ng-select.custom ::ng-deep .ng-select-container { +.clear-date-wrapper { + margin-left: -15px; +} + +.calendar-icon-wrapper { + margin-left: -5px; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.calendar-icon { + display: inline-block; + font-weight: normal; + font-size: 12px; + line-height: 1; + pointer-events: none; + color: #999999; +} + +.owl-dt-container { + font-size: inherit; +} + +.owl-dt-container-buttons, .owl-dt-container-info { + display: none; +} + +.owl-dt-timer-divider { + font-size: 12px; +} + +.ng-select.custom .ng-select-container { font-weight: normal; border: none; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0); @@ -87,30 +107,18 @@ margin-left: -10px; } -.ng-select.custom ::ng-deep .ng-option { +.ng-select.custom .ng-option { font-weight: normal; } -.ng-select.custom ::ng-deep .ng-select-container:hover { +.ng-select.custom .ng-select-container:hover, .ng-select.ng-select-focused:not(.ng-select-opened) > .ng-select-container { box-shadow: 0 1px 0 rgba(0, 0, 0, 0.075); } -.ng-select.custom ::ng-deep .ng-select-container .ng-placeholder { +.ng-select.custom .ng-select-container .ng-placeholder { font-style: italic; } .job-status-error { color: #EB5E55; } - -.job-date { - font-weight: bold; -} - -.job-id { - font-style: italic; -} - -.no-data-after-filter { - min-width: 20%; -} diff --git a/frontend/src/app/modules/job-result/job-result.component.ts b/frontend/src/app/modules/job-result/job-result.component.ts index aed4123cfe..e979fac356 100644 --- a/frontend/src/app/modules/job-result/job-result.component.ts +++ b/frontend/src/app/modules/job-result/job-result.component.ts @@ -1,4 +1,4 @@ -import {Component, OnInit} from '@angular/core'; +import {Component, OnInit, ViewChild, ViewEncapsulation} from '@angular/core'; import {JobResultDataService} from './services/job-result-data.service'; import {JobResult} from './models/job-result.model'; import {Job} from './models/job.model'; @@ -7,11 +7,16 @@ import {JobResultStatus} from './models/job-result-status.enum'; import {WptStatus} from './models/wpt-status.enum'; import {JobResultFilter} from './models/job-result-filter.model'; import {StatusService} from './services/status.service'; +import {DateTimeAdapter, OwlDateTimeComponent} from 'ng-pick-datetime'; +import {fromEvent, merge, Observable, Subscription} from 'rxjs'; +import {filter} from 'rxjs/operators'; +import {OsmLangService} from '../../services/osm-lang.service'; @Component({ selector: 'osm-job-result', templateUrl: './job-result.component.html', - styleUrls: ['./job-result.component.scss'] + styleUrls: ['./job-result.component.scss'], + encapsulation: ViewEncapsulation.None }) export class JobResultComponent implements OnInit { @@ -23,12 +28,24 @@ export class JobResultComponent implements OnInit { filteredJobResults: JobResult[] = []; selectedJob: Job = null; currentSortingRule: { [key: string]: string } = {column: 'date', direction: 'desc'}; - filter: JobResultFilter = {from: null, to: null, testAgent: '', jobResultStatus: [], wptStatus: [], description: ''}; + filter: JobResultFilter = {dateTimeRange: [], testAgent: '', jobResultStatus: [], wptStatus: [], description: ''}; + + minDate: Date; + maxDate: Date; + DateTimeRange: typeof DateTimeRange = DateTimeRange; + + @ViewChild('dateTimeRangeFrom') private dateTimeRangeFrom: OwlDateTimeComponent; + @ViewChild('dateTimeRangeTo') private dateTimeRangeTo: OwlDateTimeComponent; + private calendarClick$: Observable; + private calendarEnter$: Observable; + private calendarEventSubscription: Subscription; constructor(private dataService: JobResultDataService, public statusService: StatusService, private route: ActivatedRoute, - private router: Router) { + private router: Router, + private dateTimeAdapter: DateTimeAdapter, + private osmLangService: OsmLangService) { } jobResultStatusGroupByFn = (jobResultStatus: string): string => this.statusService.getJobResultStatusGroupName(jobResultStatus); @@ -38,6 +55,7 @@ export class JobResultComponent implements OnInit { groupValueFn = (groupName: string, children: any[]): any => ({label: groupName, children: children}); ngOnInit() { + this.setCalendarLanguage(); this.getAllJobs(); } @@ -50,6 +68,7 @@ export class JobResultComponent implements OnInit { } setJob(job: Job): void { + console.log('job', job); if (job) { this.writeQueryParams(); this.getJobResults(job.id); @@ -87,19 +106,85 @@ export class JobResultComponent implements OnInit { }); } + clearDateFilter(dateTimeComponent: DateTimeRange): void { + if (dateTimeComponent === DateTimeRange.FROM) { + const to = this.filter.dateTimeRange[1]; + this.filter.dateTimeRange = []; + this.filter.dateTimeRange[1] = to; + } + if (dateTimeComponent === DateTimeRange.TO) { + const from = this.filter.dateTimeRange[0]; + this.filter.dateTimeRange = []; + this.filter.dateTimeRange[0] = from; + } + this.applyFilter(); + } + clearTextFilter(column: string): void { this.filter[column] = ''; this.applyFilter(); } + setDateTimeRange(dateTimeComponent: DateTimeRange): void { + this.calendarEventSubscription.unsubscribe(); + + this.filter.dateTimeRange = this[dateTimeComponent].selecteds; + this.applyFilter(); + } + + observeCalendarEvents(dateTimeComponent: DateTimeRange): void { + if (!this.dateTimeRangeFrom || !this.dateTimeRangeTo) { + return; + } + + const calendarDates: HTMLElement = document.querySelector('owl-date-time-calendar'); + const dayTableDataCellsInMonthView = 'owl-date-time-month-view > table > tbody > tr > td'; + const dayElementsInMonthView = 'owl-date-time-month-view > table > tbody > tr > td > span'; + + this.calendarClick$ = fromEvent(calendarDates, 'click'); + this.calendarEnter$ = fromEvent(calendarDates, 'keydown').pipe( + filter(event => event.key === 'Enter') + ); + + this.calendarEventSubscription = merge(this.calendarClick$, this.calendarEnter$).subscribe((event: MouseEvent | KeyboardEvent) => { + if ((event.target as HTMLTableDataCellElement).matches(dayTableDataCellsInMonthView) || + (event.target as HTMLSpanElement).matches(dayElementsInMonthView) + ) { + if (dateTimeComponent === DateTimeRange.FROM) { + this.dateTimeRangeFrom.close(); + this.dateTimeRangeTo.open(); + } else if (dateTimeComponent === DateTimeRange.TO) { + this.dateTimeRangeTo.close(); + } + } + }); + } + + isDateTimeRangeSet(dateTimeComponent: DateTimeRange): boolean { + if (dateTimeComponent === DateTimeRange.FROM && this.filter.dateTimeRange[0]) { + return !isNaN(this.filter.dateTimeRange[0].valueOf()); + } + if (dateTimeComponent === DateTimeRange.TO && this.filter.dateTimeRange[1]) { + return !isNaN(this.filter.dateTimeRange[1].valueOf()); + } + return false; + } + + private setCalendarLanguage(): void { + if (this.osmLangService.getOsmLang() === 'en') { + this.dateTimeAdapter.setLocale('en-GB'); + } else { + this.dateTimeAdapter.setLocale(this.osmLangService.getOsmLang()); + } + } + private readQueryParams(): void { this.route.queryParams.subscribe((params: Params) => { if (this.checkQuery(params)) { - const jobId = parseInt(params.job, 10); + const jobId = parseInt(decodeURIComponent(params.job), 10); if (jobId) { this.selectedJob = this.jobs.find((job: Job) => job.id === jobId); - // this.filter.from = decodeURIComponent(params.from); - // this.filter.to = decodeURIComponent(params.to); + this.filter.dateTimeRange = [new Date(decodeURIComponent(params.from)), new Date (decodeURIComponent(params.to))]; this.filter.testAgent = params.testAgent ? decodeURIComponent(params.testAgent) : ''; this.filter.jobResultStatus = this.statusService.readStatusByQueryParam( params.status, JobResultStatus, this.statusService.JOB_RESULT_STATUS_GROUPS @@ -116,13 +201,17 @@ export class JobResultComponent implements OnInit { private writeQueryParams(): void { const params: Params = { - job: this.selectedJob.id, - // from: encodeURIComponent(this.filter.from), - // to: encodeURIComponent(this.filter.to), - testAgent: this.filter.testAgent !== '' ? encodeURIComponent(this.filter.testAgent) : null, - status: this.statusService.writeStatusAsQueryParam(this.filter.jobResultStatus, JobResultStatus), - wptStatus: this.statusService.writeStatusAsQueryParam(this.filter.wptStatus, WptStatus), - description: this.filter.description !== '' ? encodeURIComponent(this.filter.description) : null + job: this.selectedJob ? encodeURIComponent(this.selectedJob.id.toString(10)) : null, + from: this.selectedJob && this.filter.dateTimeRange[0] && !isNaN(this.filter.dateTimeRange[0].valueOf()) ? + encodeURIComponent(this.filter.dateTimeRange[0].toISOString()) : + null, + to: this.selectedJob && this.filter.dateTimeRange[1] && !isNaN(this.filter.dateTimeRange[1].valueOf()) ? + encodeURIComponent(this.filter.dateTimeRange[1].toISOString()) : + null, + testAgent: this.selectedJob && this.filter.testAgent !== '' ? encodeURIComponent(this.filter.testAgent) : null, + status: this.statusService.writeStatusAsQueryParam(this.filter.jobResultStatus, JobResultStatus, !!this.selectedJob), + wptStatus: this.statusService.writeStatusAsQueryParam(this.filter.wptStatus, WptStatus, !!this.selectedJob), + description: this.selectedJob && this.filter.description !== '' ? encodeURIComponent(this.filter.description) : null }; this.router.navigate([], { @@ -136,6 +225,7 @@ export class JobResultComponent implements OnInit { .subscribe((jobResults: JobResult[]) => { this.jobResults = jobResults; this.filteredJobResults = jobResults; + this.setMinAndMaxDate(jobResults); this.applyFilter(); }); } @@ -188,6 +278,14 @@ export class JobResultComponent implements OnInit { } private isJobResultIncludingTerms(jobResult: JobResult): boolean { + if (jobResult.date && this.filter.dateTimeRange.length > 0) { + if (this.filter.dateTimeRange[0] && jobResult.date < this.filter.dateTimeRange[0]) { + return false; + } + if (this.filter.dateTimeRange[1] && jobResult.date > this.filter.dateTimeRange[1]) { + return false; + } + } if (jobResult.testAgent && !jobResult.testAgent.toLowerCase().includes(this.filter.testAgent.toLowerCase())) { return false; } @@ -206,4 +304,14 @@ export class JobResultComponent implements OnInit { } return true; } + + private setMinAndMaxDate(jobResults: JobResult[]): void { + this.minDate = jobResults && jobResults.length > 0 ? jobResults[jobResults.length - 1].date : null; + this.maxDate = jobResults && jobResults.length > 0 ? jobResults[0].date : null; + } +} + +export enum DateTimeRange { + FROM = 'dateTimeRangeFrom', + TO = 'dateTimeRangeTo' } diff --git a/frontend/src/app/modules/job-result/job-result.module.ts b/frontend/src/app/modules/job-result/job-result.module.ts index 1806114364..6cd3b981d2 100644 --- a/frontend/src/app/modules/job-result/job-result.module.ts +++ b/frontend/src/app/modules/job-result/job-result.module.ts @@ -5,20 +5,24 @@ import {RouterModule, Routes} from '@angular/router'; import {SharedModule} from '../shared/shared.module'; import {FormsModule} from '@angular/forms'; import {NgSelectModule} from '@ng-select/ng-select'; +import {OwlDateTimeModule, OwlNativeDateTimeModule} from 'ng-pick-datetime'; const JobResultRoutes: Routes = [ - {path: 'list', component: JobResultComponent}, + {path: 'list', component: JobResultComponent, data: {title: 'frontend.de.iteratec.osm.jobResult.title'}}, + {path: '**', redirectTo: 'list', pathMatch: 'full'} ]; @NgModule({ declarations: [JobResultComponent], - imports: [ - CommonModule, - RouterModule.forChild(JobResultRoutes), - SharedModule, - FormsModule, - NgSelectModule - ] + imports: [ + CommonModule, + RouterModule.forChild(JobResultRoutes), + SharedModule, + FormsModule, + NgSelectModule, + OwlDateTimeModule, + OwlNativeDateTimeModule + ] }) export class JobResultModule { } diff --git a/frontend/src/app/modules/job-result/models/job-result-filter.model.ts b/frontend/src/app/modules/job-result/models/job-result-filter.model.ts index 7ebdac8764..2e9e8e3da3 100644 --- a/frontend/src/app/modules/job-result/models/job-result-filter.model.ts +++ b/frontend/src/app/modules/job-result/models/job-result-filter.model.ts @@ -1,6 +1,5 @@ export interface JobResultFilter { - from: Date; - to: Date; + dateTimeRange: Date[]; testAgent: string; jobResultStatus: (string | object)[]; wptStatus: (string | object)[]; diff --git a/frontend/src/app/modules/job-result/models/job-result.model.ts b/frontend/src/app/modules/job-result/models/job-result.model.ts index 58693abf2e..eeee45204a 100644 --- a/frontend/src/app/modules/job-result/models/job-result.model.ts +++ b/frontend/src/app/modules/job-result/models/job-result.model.ts @@ -1,8 +1,7 @@ export interface JobResult { - testId: string; testUrl: string; - testAgent: string; date: Date; + testAgent: string; jobResultStatus: string; wptStatus: string; description: string; diff --git a/frontend/src/app/modules/job-result/services/status.service.ts b/frontend/src/app/modules/job-result/services/status.service.ts index f7bbfe7298..6fd3e1a2b2 100644 --- a/frontend/src/app/modules/job-result/services/status.service.ts +++ b/frontend/src/app/modules/job-result/services/status.service.ts @@ -79,15 +79,19 @@ export class StatusService { } } - writeStatusAsQueryParam(statusList: (string | object)[], enumeration: any): string[] { - if (statusList.length > 0) { + writeStatusAsQueryParam(statusList: (string | object)[], enumeration: any, isJobSelected: boolean): string[] { + if (isJobSelected && statusList.length > 0) { return statusList.map(status => { if (typeof status === 'string') { - return Object.keys(enumeration).find(key => enumeration[key] === status).toLowerCase(); + return encodeURIComponent( + Object.keys(enumeration).find(key => enumeration[key] === status).toLowerCase() + ); } if (typeof status === 'object' && status['label'] && typeof status['label'] === 'string') { - return Object.keys(StatusGroup).find(key => - this.translateService.instant(StatusGroup[key]) === status['label']).toLowerCase(); + return encodeURIComponent( + Object.keys(StatusGroup).find(key => + this.translateService.instant(StatusGroup[key]) === status['label']).toLowerCase() + ); } }); } @@ -101,7 +105,7 @@ export class StatusService { return []; } if (typeof statusParam === 'string') { - statusParam = statusParam.toUpperCase(); + statusParam = decodeURIComponent(statusParam).toUpperCase(); if (Object.keys(StatusGroup).find(key => key === statusParam)) { return [].concat({ label: this.translateService.instant(StatusGroup[statusParam]), @@ -112,7 +116,7 @@ export class StatusService { } if (Array.isArray(statusParam)) { return statusParam.map((status: string) => { - status = status.toUpperCase(); + status = decodeURIComponent(status).toUpperCase(); if (Object.keys(StatusGroup).find(key => key === status)) { return { label: this.translateService.instant(StatusGroup[status]), diff --git a/frontend/src/app/modules/landing/landing.component.html b/frontend/src/app/modules/landing/landing.component.html index e57523b3f6..efa22a5ea7 100644 --- a/frontend/src/app/modules/landing/landing.component.html +++ b/frontend/src/app/modules/landing/landing.component.html @@ -40,7 +40,7 @@

{{ 'frontend.de.iteratec.osm.landing.healthIssues' | translate }}

  • - +
  • -
  • - - - - +
  • + + + +
  • diff --git a/grails-app/views/job/_jobTable.gsp b/grails-app/views/job/_jobTable.gsp index 58b3ab8814..97a7d56529 100644 --- a/grails-app/views/job/_jobTable.gsp +++ b/grails-app/views/job/_jobTable.gsp @@ -55,7 +55,7 @@ - From df94c83fdab273d40bc7bc088c3f19af600ae131 Mon Sep 17 00:00:00 2001 From: Daniel Steger Date: Mon, 22 Jun 2020 12:07:16 +0200 Subject: [PATCH 11/14] Fix tests --- .../app/modules/job-result/job-result.component.spec.ts | 7 ++++++- .../src/app/modules/job-result/job-result.component.ts | 1 - .../modules/job-result/services/status.service.spec.ts | 9 ++++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/modules/job-result/job-result.component.spec.ts b/frontend/src/app/modules/job-result/job-result.component.spec.ts index 28763628c5..9ccb922f31 100644 --- a/frontend/src/app/modules/job-result/job-result.component.spec.ts +++ b/frontend/src/app/modules/job-result/job-result.component.spec.ts @@ -3,6 +3,8 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import {JobResultComponent} from './job-result.component'; import {SharedModule} from '../shared/shared.module'; import {SharedMocksModule} from '../../testing/shared-mocks.module'; +import {OsmLangService} from '../../services/osm-lang.service'; +import {GrailsBridgeService} from '../../services/grails-bridge.service'; describe('JobResultComponent', () => { let component: JobResultComponent; @@ -12,8 +14,11 @@ describe('JobResultComponent', () => { TestBed.configureTestingModule({ declarations: [JobResultComponent], imports: [ - SharedModule, SharedMocksModule + ], + providers: [ + OsmLangService, + GrailsBridgeService ] }) .compileComponents(); diff --git a/frontend/src/app/modules/job-result/job-result.component.ts b/frontend/src/app/modules/job-result/job-result.component.ts index e979fac356..9285ec52f8 100644 --- a/frontend/src/app/modules/job-result/job-result.component.ts +++ b/frontend/src/app/modules/job-result/job-result.component.ts @@ -68,7 +68,6 @@ export class JobResultComponent implements OnInit { } setJob(job: Job): void { - console.log('job', job); if (job) { this.writeQueryParams(); this.getJobResults(job.id); diff --git a/frontend/src/app/modules/job-result/services/status.service.spec.ts b/frontend/src/app/modules/job-result/services/status.service.spec.ts index e526e93672..92599aa471 100644 --- a/frontend/src/app/modules/job-result/services/status.service.spec.ts +++ b/frontend/src/app/modules/job-result/services/status.service.spec.ts @@ -1,9 +1,12 @@ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { StatusService } from './status.service'; +import {StatusService} from './status.service'; +import {SharedMocksModule} from '../../../testing/shared-mocks.module'; describe('StatusService', () => { - beforeEach(() => TestBed.configureTestingModule({})); + beforeEach(() => TestBed.configureTestingModule({ + imports: [SharedMocksModule] + })); it('should be created', () => { const service: StatusService = TestBed.get(StatusService); From ae77d60f724c866527f730846dd34a4787133329 Mon Sep 17 00:00:00 2001 From: Daniel Steger Date: Mon, 22 Jun 2020 13:11:04 +0200 Subject: [PATCH 12/14] Accept any date when filtering for dates --- .../src/app/modules/job-result/job-result.component.html | 8 ++++---- .../src/app/modules/job-result/job-result.component.ts | 8 -------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/modules/job-result/job-result.component.html b/frontend/src/app/modules/job-result/job-result.component.html index 67ea0f19e4..ea77060546 100644 --- a/frontend/src/app/modules/job-result/job-result.component.html +++ b/frontend/src/app/modules/job-result/job-result.component.html @@ -37,8 +37,8 @@

    - - diff --git a/frontend/src/app/modules/job-result/job-result.component.ts b/frontend/src/app/modules/job-result/job-result.component.ts index 9285ec52f8..323224cad8 100644 --- a/frontend/src/app/modules/job-result/job-result.component.ts +++ b/frontend/src/app/modules/job-result/job-result.component.ts @@ -30,8 +30,6 @@ export class JobResultComponent implements OnInit { currentSortingRule: { [key: string]: string } = {column: 'date', direction: 'desc'}; filter: JobResultFilter = {dateTimeRange: [], testAgent: '', jobResultStatus: [], wptStatus: [], description: ''}; - minDate: Date; - maxDate: Date; DateTimeRange: typeof DateTimeRange = DateTimeRange; @ViewChild('dateTimeRangeFrom') private dateTimeRangeFrom: OwlDateTimeComponent; @@ -224,7 +222,6 @@ export class JobResultComponent implements OnInit { .subscribe((jobResults: JobResult[]) => { this.jobResults = jobResults; this.filteredJobResults = jobResults; - this.setMinAndMaxDate(jobResults); this.applyFilter(); }); } @@ -303,11 +300,6 @@ export class JobResultComponent implements OnInit { } return true; } - - private setMinAndMaxDate(jobResults: JobResult[]): void { - this.minDate = jobResults && jobResults.length > 0 ? jobResults[jobResults.length - 1].date : null; - this.maxDate = jobResults && jobResults.length > 0 ? jobResults[0].date : null; - } } export enum DateTimeRange { From 38addfa1288975cc5e49648434411d9dedf72657 Mon Sep 17 00:00:00 2001 From: Daniel Steger Date: Wed, 24 Jun 2020 13:24:53 +0200 Subject: [PATCH 13/14] Make status service private --- .../modules/job-result/job-result.component.html | 6 +++--- .../app/modules/job-result/job-result.component.ts | 14 +++++++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/modules/job-result/job-result.component.html b/frontend/src/app/modules/job-result/job-result.component.html index ea77060546..c02e7ee353 100644 --- a/frontend/src/app/modules/job-result/job-result.component.html +++ b/frontend/src/app/modules/job-result/job-result.component.html @@ -153,9 +153,9 @@

    {{ 'frontend.de.iteratec.osm.jobResult.tableHead.date' | translate }} @@ -40,7 +41,8 @@

    {{ 'frontend.de.iteratec.osm.jobResult.tableHead.testId' | translate }}

    + + {{ 'frontend.de.iteratec.osm.jobResult.tableHead.testAgent' | translate }} - - +
    + + + + +
    + + {{ 'frontend.de.iteratec.osm.jobResult.tableHead.jobResultStatus' | translate }} - typeToSearchText="{{ 'frontend.de.iteratec.osm.jobResult.select.search' | translate }}"> + + {{ 'frontend.de.iteratec.osm.jobResult.tableHead.wptStatus' | translate }} - typeToSearchText="{{ 'frontend.de.iteratec.osm.jobResult.select.search' | translate }}"> + + {{ 'frontend.de.iteratec.osm.jobResult.tableHead.description' | translate }} - +
    + + + + +
    -
    {{ test.date | date:('frontend.de.iteratec.osm.jobResult.dateFormat' | translate) }}
    diff --git a/frontend/src/app/modules/job-result/job-result.component.scss b/frontend/src/app/modules/job-result/job-result.component.scss index 9a1f407da1..42bef9bdc0 100644 --- a/frontend/src/app/modules/job-result/job-result.component.scss +++ b/frontend/src/app/modules/job-result/job-result.component.scss @@ -6,14 +6,18 @@ margin-bottom: 20px; } -.sortable { - cursor: pointer; -} - .date-table-head { padding-bottom: 23px; } +.table-head { + max-width: 15em; +} + +.sortable { + cursor: pointer; +} + .job-id { font-style: italic; font-weight: normal; @@ -26,7 +30,12 @@ opacity: 0.8; } +.input-container { + // display: inline; +} + .input-filter { + display: inline; font-weight: normal; border: none; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0); @@ -35,11 +44,11 @@ margin-left: -12px; } -.input-filter:hover { +.input-container:hover .input-filter { box-shadow: 0 1px 0 rgba(0, 0, 0, 0.075); } -.input-filter:focus { +.input-filter:focus, .input-container:hover .input-filter:focus { box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(0, 0, 0, 0.075); } @@ -47,8 +56,26 @@ font-style: italic; } -.status-table-head { - max-width: 15em; +.clear-input-wrapper { + margin-left: -30px; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.clear-input { + display: inline-block; + font-weight: normal; + font-size: 18px; + line-height: 1; + pointer-events: none; + color: #999999; +} + +.clear-input-wrapper:hover .clear-input { + color: #D0021B } .ng-select.custom ::ng-deep .ng-select-container { diff --git a/frontend/src/app/modules/job-result/job-result.component.ts b/frontend/src/app/modules/job-result/job-result.component.ts index 55af819136..aed4123cfe 100644 --- a/frontend/src/app/modules/job-result/job-result.component.ts +++ b/frontend/src/app/modules/job-result/job-result.component.ts @@ -3,11 +3,10 @@ import {JobResultDataService} from './services/job-result-data.service'; import {JobResult} from './models/job-result.model'; import {Job} from './models/job.model'; import {ActivatedRoute, Params, Router} from '@angular/router'; -import {TranslateService} from '@ngx-translate/core'; -import {StatusGroup} from './models/status-group.enum'; import {JobResultStatus} from './models/job-result-status.enum'; import {WptStatus} from './models/wpt-status.enum'; import {JobResultFilter} from './models/job-result-filter.model'; +import {StatusService} from './services/status.service'; @Component({ selector: 'osm-job-result', @@ -26,58 +25,15 @@ export class JobResultComponent implements OnInit { currentSortingRule: { [key: string]: string } = {column: 'date', direction: 'desc'}; filter: JobResultFilter = {from: null, to: null, testAgent: '', jobResultStatus: [], wptStatus: [], description: ''}; - JOB_RESULT_STATUS_GROUPS: {[key: string]: string []} = { - GROUP_NOT_TERMINATED: [ - JobResultStatus.WAITING, - JobResultStatus.RUNNING - ], - GROUP_SUCCESS: [ - JobResultStatus.SUCCESS - ], - GROUP_FAILED: [ - JobResultStatus.INCOMPLETE, - JobResultStatus.LAUNCH_ERROR, - JobResultStatus.FETCH_ERROR, - JobResultStatus.PERSISTENCE_ERROR, - JobResultStatus.TIMEOUT, - JobResultStatus.FAILED, - JobResultStatus.CANCELED, - JobResultStatus.ORPHANED, - JobResultStatus.DID_NOT_START - ] - }; - - WPT_STATUS_GROUPS: {[key: string]: string []} = { - GROUP_NOT_TERMINATED: [ - WptStatus.UNKNOWN, - WptStatus.PENDING, - WptStatus.IN_PROGRESS - ], - GROUP_SUCCESS: [ - WptStatus.SUCCESSFUL, - WptStatus.COMPLETED, - WptStatus.TEST_HAS_A_UNDEFINED_PROBLEM, - WptStatus.TEST_COMPLETED_BUT_INDIVIDUAL_REQUEST_FAILED - ], - GROUP_FAILED: [ - WptStatus.TESTED_APPLICATION_CLIENT_ERROR, - WptStatus.TESTED_APPLICATION_INTERNAL_SERVER_ERROR, - WptStatus.TEST_DID_NOT_START, - WptStatus.TEST_FAILED_WAITING_FOR_DOM_ELEMENT, - WptStatus.TEST_TIMED_OUT, - WptStatus.TEST_TIMED_OUT_CONTENT_ERRORS - ] - }; - constructor(private dataService: JobResultDataService, + public statusService: StatusService, private route: ActivatedRoute, - private router: Router, - private translateService: TranslateService) { + private router: Router) { } - jobResultStatusGroupByFn = (jobResultStatus: string): string => this.getJobResultStatusGroupName(jobResultStatus); + jobResultStatusGroupByFn = (jobResultStatus: string): string => this.statusService.getJobResultStatusGroupName(jobResultStatus); - wptStatusGroupByFn = (wptStatus: string): string => this.getWptStatusGroupName(wptStatus); + wptStatusGroupByFn = (wptStatus: string): string => this.statusService.getWptStatusGroupName(wptStatus); groupValueFn = (groupName: string, children: any[]): any => ({label: groupName, children: children}); @@ -121,7 +77,7 @@ export class JobResultComponent implements OnInit { ); } - setFilter(): void { + applyFilter(): void { this.writeQueryParams(); this.filteredJobResults = this.jobResults.filter((jobResult: JobResult) => { if (this.arePropertiesThatAreFilteredEmpty(jobResult)) { @@ -131,40 +87,9 @@ export class JobResultComponent implements OnInit { }); } - isTestNotTerminated(jobResultStatus: string): boolean { - return this.JOB_RESULT_STATUS_GROUPS.GROUP_NOT_TERMINATED.includes(jobResultStatus); - } - - isTestSuccessful(jobResultStatus: string): boolean { - return this.JOB_RESULT_STATUS_GROUPS.GROUP_SUCCESS.includes(jobResultStatus); - } - - hasTestFailed(jobResultStatus: string): boolean { - return this.JOB_RESULT_STATUS_GROUPS.GROUP_FAILED.includes(jobResultStatus); - } - - private getJobResultStatusGroupName(jobResultStatus: string): string { - if (this.isTestNotTerminated(jobResultStatus)) { - return this.translateService.instant(StatusGroup.GROUP_NOT_TERMINATED); - } - if (this.isTestSuccessful(jobResultStatus)) { - return this.translateService.instant(StatusGroup.GROUP_SUCCESS); - } - if (this.hasTestFailed(jobResultStatus)) { - return this.translateService.instant(StatusGroup.GROUP_FAILED); - } - } - - private getWptStatusGroupName(wptStatus: string): string { - if (this.isWptNotTerminated(wptStatus)) { - return this.translateService.instant(StatusGroup.GROUP_NOT_TERMINATED); - } - if (this.isWptSuccessful(wptStatus)) { - return this.translateService.instant(StatusGroup.GROUP_SUCCESS); - } - if (this.hasWptFailed(wptStatus)) { - return this.translateService.instant(StatusGroup.GROUP_FAILED); - } + clearTextFilter(column: string): void { + this.filter[column] = ''; + this.applyFilter(); } private readQueryParams(): void { @@ -176,8 +101,12 @@ export class JobResultComponent implements OnInit { // this.filter.from = decodeURIComponent(params.from); // this.filter.to = decodeURIComponent(params.to); this.filter.testAgent = params.testAgent ? decodeURIComponent(params.testAgent) : ''; - this.filter.jobResultStatus = this.readStatusByQueryParam(params.status, JobResultStatus, this.JOB_RESULT_STATUS_GROUPS); - this.filter.wptStatus = this.readStatusByQueryParam(params.wptStatus, WptStatus, this.WPT_STATUS_GROUPS); + this.filter.jobResultStatus = this.statusService.readStatusByQueryParam( + params.status, JobResultStatus, this.statusService.JOB_RESULT_STATUS_GROUPS + ); + this.filter.wptStatus = this.statusService.readStatusByQueryParam( + params.wptStatus, WptStatus, this.statusService.WPT_STATUS_GROUPS + ); this.filter.description = params.description ? decodeURIComponent(params.description) : ''; this.getJobResults(jobId); } @@ -191,8 +120,8 @@ export class JobResultComponent implements OnInit { // from: encodeURIComponent(this.filter.from), // to: encodeURIComponent(this.filter.to), testAgent: this.filter.testAgent !== '' ? encodeURIComponent(this.filter.testAgent) : null, - status: this.writeStatusAsQueryParam(this.filter.jobResultStatus, JobResultStatus), - wptStatus: this.writeStatusAsQueryParam(this.filter.wptStatus, WptStatus), + status: this.statusService.writeStatusAsQueryParam(this.filter.jobResultStatus, JobResultStatus), + wptStatus: this.statusService.writeStatusAsQueryParam(this.filter.wptStatus, WptStatus), description: this.filter.description !== '' ? encodeURIComponent(this.filter.description) : null }; @@ -207,7 +136,7 @@ export class JobResultComponent implements OnInit { .subscribe((jobResults: JobResult[]) => { this.jobResults = jobResults; this.filteredJobResults = jobResults; - this.setFilter(); + this.applyFilter(); }); } @@ -263,12 +192,12 @@ export class JobResultComponent implements OnInit { return false; } if (jobResult.jobResultStatus && this.filter.jobResultStatus.length > 0) { - if (!this.isStatusIncludingTerms(jobResult.jobResultStatus, this.filter.jobResultStatus)) { + if (!this.statusService.isStatusIncludingTerms(jobResult.jobResultStatus, this.filter.jobResultStatus)) { return false; } } if (jobResult.wptStatus && this.filter.wptStatus.length > 0) { - if (!this.isStatusIncludingTerms(jobResult.wptStatus, this.filter.wptStatus)) { + if (!this.statusService.isStatusIncludingTerms(jobResult.wptStatus, this.filter.wptStatus)) { return false; } } @@ -277,74 +206,4 @@ export class JobResultComponent implements OnInit { } return true; } - - private isStatusIncludingTerms(status: string, terms: any): boolean { - return terms.find((termOrTermList: (string | string[])) => { - if (typeof termOrTermList === 'string') { - return status.toLowerCase().includes(termOrTermList.toLowerCase()); - } - if (typeof termOrTermList === 'object' && termOrTermList['children'] && Array.isArray(termOrTermList['children'])) { - return termOrTermList['children'].find((term: string) => status.toLowerCase().includes(term.toLowerCase())); - } - return false; - }); - } - - private isWptNotTerminated(wptStatus: string): boolean { - return this.WPT_STATUS_GROUPS.GROUP_NOT_TERMINATED.includes(wptStatus); - } - - private isWptSuccessful(wptStatus: string): boolean { - return this.WPT_STATUS_GROUPS.GROUP_SUCCESS.includes(wptStatus); - } - - private hasWptFailed(wptStatus: string): boolean { - return this.WPT_STATUS_GROUPS.GROUP_FAILED.includes(wptStatus); - } - - private writeStatusAsQueryParam(statusList: (string | object)[], enumeration: any): string[] { - if (statusList.length > 0) { - return statusList.map(status => { - if (typeof status === 'string') { - return Object.keys(enumeration).find(key => enumeration[key] === status).toLowerCase(); - } - if (typeof status === 'object' && status['label'] && typeof status['label'] === 'string') { - return Object.keys(StatusGroup).find(key => - this.translateService.instant(StatusGroup[key]) === status['label']).toLowerCase(); - } - }); - } - return null; - } - - private readStatusByQueryParam(statusParam: (string | string[]), - enumeration: any, - statusGroupType: {[key: string]: string []} - ): (string | object)[] { - if (!statusParam) { - return []; - } - if (typeof statusParam === 'string') { - statusParam = statusParam.toUpperCase(); - if (Object.keys(StatusGroup).find(key => key === statusParam)) { - return [].concat({ - label: this.translateService.instant(StatusGroup[statusParam]), - children: statusGroupType[statusParam] - }); - } - return [].concat(enumeration[statusParam]); - } - if (Array.isArray(statusParam)) { - return statusParam.map((status: string) => { - status = status.toUpperCase(); - if (Object.keys(StatusGroup).find(key => key === status)) { - return { - label: this.translateService.instant(StatusGroup[status]), - children: statusGroupType[status] - }; - } - return enumeration[status]; - }); - } - } } diff --git a/frontend/src/app/modules/job-result/services/status.service.spec.ts b/frontend/src/app/modules/job-result/services/status.service.spec.ts new file mode 100644 index 0000000000..e526e93672 --- /dev/null +++ b/frontend/src/app/modules/job-result/services/status.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { StatusService } from './status.service'; + +describe('StatusService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: StatusService = TestBed.get(StatusService); + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/modules/job-result/services/status.service.ts b/frontend/src/app/modules/job-result/services/status.service.ts new file mode 100644 index 0000000000..f7bbfe7298 --- /dev/null +++ b/frontend/src/app/modules/job-result/services/status.service.ts @@ -0,0 +1,162 @@ +import {Injectable} from '@angular/core'; +import {JobResultStatus} from '../models/job-result-status.enum'; +import {WptStatus} from '../models/wpt-status.enum'; +import {StatusGroup} from '../models/status-group.enum'; +import {TranslateService} from '@ngx-translate/core'; + +@Injectable({ + providedIn: 'root' +}) +export class StatusService { + + readonly JOB_RESULT_STATUS_GROUPS: { [key: string]: string [] } = { + GROUP_NOT_TERMINATED: [ + JobResultStatus.WAITING, + JobResultStatus.RUNNING + ], + GROUP_SUCCESS: [ + JobResultStatus.SUCCESS + ], + GROUP_FAILED: [ + JobResultStatus.INCOMPLETE, + JobResultStatus.LAUNCH_ERROR, + JobResultStatus.FETCH_ERROR, + JobResultStatus.PERSISTENCE_ERROR, + JobResultStatus.TIMEOUT, + JobResultStatus.FAILED, + JobResultStatus.CANCELED, + JobResultStatus.ORPHANED, + JobResultStatus.DID_NOT_START + ] + }; + + readonly WPT_STATUS_GROUPS: { [key: string]: string [] } = { + GROUP_NOT_TERMINATED: [ + WptStatus.UNKNOWN, + WptStatus.PENDING, + WptStatus.IN_PROGRESS + ], + GROUP_SUCCESS: [ + WptStatus.SUCCESSFUL, + WptStatus.COMPLETED, + WptStatus.TEST_HAS_A_UNDEFINED_PROBLEM, + WptStatus.TEST_COMPLETED_BUT_INDIVIDUAL_REQUEST_FAILED + ], + GROUP_FAILED: [ + WptStatus.TESTED_APPLICATION_CLIENT_ERROR, + WptStatus.TESTED_APPLICATION_INTERNAL_SERVER_ERROR, + WptStatus.TEST_DID_NOT_START, + WptStatus.TEST_FAILED_WAITING_FOR_DOM_ELEMENT, + WptStatus.TEST_TIMED_OUT, + WptStatus.TEST_TIMED_OUT_CONTENT_ERRORS + ] + }; + + constructor(private translateService: TranslateService) { + } + + getJobResultStatusGroupName(jobResultStatus: string): string { + if (this.isTestNotTerminated(jobResultStatus)) { + return this.translateService.instant(StatusGroup.GROUP_NOT_TERMINATED); + } + if (this.isTestSuccessful(jobResultStatus)) { + return this.translateService.instant(StatusGroup.GROUP_SUCCESS); + } + if (this.hasTestFailed(jobResultStatus)) { + return this.translateService.instant(StatusGroup.GROUP_FAILED); + } + } + + getWptStatusGroupName(wptStatus: string): string { + if (this.isWptNotTerminated(wptStatus)) { + return this.translateService.instant(StatusGroup.GROUP_NOT_TERMINATED); + } + if (this.isWptSuccessful(wptStatus)) { + return this.translateService.instant(StatusGroup.GROUP_SUCCESS); + } + if (this.hasWptFailed(wptStatus)) { + return this.translateService.instant(StatusGroup.GROUP_FAILED); + } + } + + writeStatusAsQueryParam(statusList: (string | object)[], enumeration: any): string[] { + if (statusList.length > 0) { + return statusList.map(status => { + if (typeof status === 'string') { + return Object.keys(enumeration).find(key => enumeration[key] === status).toLowerCase(); + } + if (typeof status === 'object' && status['label'] && typeof status['label'] === 'string') { + return Object.keys(StatusGroup).find(key => + this.translateService.instant(StatusGroup[key]) === status['label']).toLowerCase(); + } + }); + } + return null; + } + + readStatusByQueryParam(statusParam: (string | string[]), + enumeration: any, + statusGroupType: { [key: string]: string [] }): (string | object)[] { + if (!statusParam) { + return []; + } + if (typeof statusParam === 'string') { + statusParam = statusParam.toUpperCase(); + if (Object.keys(StatusGroup).find(key => key === statusParam)) { + return [].concat({ + label: this.translateService.instant(StatusGroup[statusParam]), + children: statusGroupType[statusParam] + }); + } + return [].concat(enumeration[statusParam]); + } + if (Array.isArray(statusParam)) { + return statusParam.map((status: string) => { + status = status.toUpperCase(); + if (Object.keys(StatusGroup).find(key => key === status)) { + return { + label: this.translateService.instant(StatusGroup[status]), + children: statusGroupType[status] + }; + } + return enumeration[status]; + }); + } + } + + isStatusIncludingTerms(status: string, terms: any): boolean { + return terms.find((termOrTermList: (string | string[])) => { + if (typeof termOrTermList === 'string') { + return status.toLowerCase().includes(termOrTermList.toLowerCase()); + } + if (typeof termOrTermList === 'object' && termOrTermList['children'] && Array.isArray(termOrTermList['children'])) { + return termOrTermList['children'].find((term: string) => status.toLowerCase().includes(term.toLowerCase())); + } + return false; + }); + } + + isTestNotTerminated(jobResultStatus: string): boolean { + return this.JOB_RESULT_STATUS_GROUPS.GROUP_NOT_TERMINATED.includes(jobResultStatus); + } + + isTestSuccessful(jobResultStatus: string): boolean { + return this.JOB_RESULT_STATUS_GROUPS.GROUP_SUCCESS.includes(jobResultStatus); + } + + hasTestFailed(jobResultStatus: string): boolean { + return this.JOB_RESULT_STATUS_GROUPS.GROUP_FAILED.includes(jobResultStatus); + } + + private isWptNotTerminated(wptStatus: string): boolean { + return this.WPT_STATUS_GROUPS.GROUP_NOT_TERMINATED.includes(wptStatus); + } + + private isWptSuccessful(wptStatus: string): boolean { + return this.WPT_STATUS_GROUPS.GROUP_SUCCESS.includes(wptStatus); + } + + private hasWptFailed(wptStatus: string): boolean { + return this.WPT_STATUS_GROUPS.GROUP_FAILED.includes(wptStatus); + } +} From 16e807d722ef0aa80f7d825285a5a32ee11b6c88 Mon Sep 17 00:00:00 2001 From: Daniel Steger Date: Mon, 22 Jun 2020 11:45:54 +0200 Subject: [PATCH 10/14] Filter by date etc. --- frontend/src/app/app-routing.module.ts | 2 +- .../job-result/job-result.component.html | 50 +++++-- .../job-result/job-result.component.scss | 82 ++++++----- .../job-result/job-result.component.ts | 136 ++++++++++++++++-- .../modules/job-result/job-result.module.ts | 20 +-- .../models/job-result-filter.model.ts | 3 +- .../job-result/models/job-result.model.ts | 3 +- .../job-result/services/status.service.ts | 18 ++- .../modules/landing/landing.component.html | 2 +- grails-app/conf/application.yml | 1 - .../de/iteratec/osm/UrlMappings.groovy | 2 +- grails-app/i18n/messages.properties | 4 +- grails-app/i18n/messages_de.properties | 4 +- grails-app/views/_menu/_navbar.gsp | 11 +- grails-app/views/job/_jobTable.gsp | 2 +- 15 files changed, 244 insertions(+), 96 deletions(-) diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 77b8f871ca..87d4bf4397 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -37,7 +37,7 @@ const appRoutes: Routes = [ loadChildren: './modules/distribution/distribution.module#DistributionModule' }, { - path: 'jobResultDev', + path: 'jobResult', loadChildren: './modules/job-result/job-result.module#JobResultModule' }, { diff --git a/frontend/src/app/modules/job-result/job-result.component.html b/frontend/src/app/modules/job-result/job-result.component.html index 39046c268f..67ea0f19e4 100644 --- a/frontend/src/app/modules/job-result/job-result.component.html +++ b/frontend/src/app/modules/job-result/job-result.component.html @@ -27,7 +27,7 @@

    + {{ 'frontend.de.iteratec.osm.jobResult.tableHead.date' | translate }} - -
    - - {{ 'frontend.de.iteratec.osm.jobResult.tableHead.testId' | translate }} - +
    + + + + + + + + + + + + + + + + +
    @@ -75,7 +103,7 @@

    [groupBy]="jobResultStatusGroupByFn" [groupValue]="groupValueFn" [items]="allJobResultStatus" [multiple]="true" [selectableGroup]="true" class="custom" clearAllText="{{ 'frontend.de.iteratec.osm.jobResult.select.clearAll' | translate }}" - notFoundText="{{ 'frontend.de.iteratec.osm.jobResult.filter.notFound' | translate }}" + notFoundText="-" placeholder="{{ 'frontend.de.iteratec.osm.jobResult.filter.placeholder' | translate }}" typeToSearchText="{{ 'frontend.de.iteratec.osm.jobResult.select.search' | translate }}"> @@ -93,7 +121,7 @@

    [groupBy]="wptStatusGroupByFn" [groupValue]="groupValueFn" [items]="allWptStatus" [multiple]="true" [selectableGroup]="true" class="custom" clearAllText="{{ 'frontend.de.iteratec.osm.jobResult.select.clearAll' | translate }}" - notFoundText="{{ 'frontend.de.iteratec.osm.jobResult.filter.notFound' | translate }}" + notFoundText="-" placeholder="{{ 'frontend.de.iteratec.osm.jobResult.filter.placeholder' | translate }}" typeToSearchText="{{ 'frontend.de.iteratec.osm.jobResult.select.search' | translate }}"> @@ -129,9 +157,7 @@

    [class.text-info]="statusService.isTestNotTerminated(test.jobResultStatus)" [class.text-success]="statusService.isTestSuccessful(test.jobResultStatus)" href="{{ test.testUrl }}"> -
    {{ test.date | date:('frontend.de.iteratec.osm.jobResult.dateFormat' | translate) }}
    -
    {{ test.testId }}
    +
    {{ test.date | date:('frontend.de.iteratec.osm.jobResult.dateFormat' | translate) }}
    @@ -141,7 +167,7 @@

    {{ test.description ? test.description : '-' }}
    -- - - -
    -
    {{ test.date | date:('frontend.de.iteratec.osm.jobResult.dateFormat' | translate) }}
    diff --git a/frontend/src/app/modules/job-result/job-result.component.ts b/frontend/src/app/modules/job-result/job-result.component.ts index 323224cad8..d6cc684a63 100644 --- a/frontend/src/app/modules/job-result/job-result.component.ts +++ b/frontend/src/app/modules/job-result/job-result.component.ts @@ -39,7 +39,7 @@ export class JobResultComponent implements OnInit { private calendarEventSubscription: Subscription; constructor(private dataService: JobResultDataService, - public statusService: StatusService, + private statusService: StatusService, private route: ActivatedRoute, private router: Router, private dateTimeAdapter: DateTimeAdapter, @@ -167,6 +167,18 @@ export class JobResultComponent implements OnInit { return false; } + isTestNotTerminated(jobResultStatus: string): boolean { + return this.statusService.isTestNotTerminated(jobResultStatus); + } + + isTestSuccessful(jobResultStatus: string): boolean { + return this.statusService.isTestSuccessful(jobResultStatus); + } + + hasTestFailed(jobResultStatus: string): boolean { + return this.statusService.hasTestFailed(jobResultStatus); + } + private setCalendarLanguage(): void { if (this.osmLangService.getOsmLang() === 'en') { this.dateTimeAdapter.setLocale('en-GB'); From a7a72c5c435671fc0d552d8cb104830898e48e0a Mon Sep 17 00:00:00 2001 From: Daniel Steger Date: Tue, 30 Jun 2020 18:30:21 +0200 Subject: [PATCH 14/14] Fixed link for failed jobs. Fixed date format. Fixed ng-select behaviour. --- frontend/package-lock.json | 6 +- frontend/package.json | 2 +- .../job-result/job-result.component.html | 4 +- .../job-result/job-result.component.ts | 66 +++++++++-------- .../job-result/services/status.service.ts | 73 +++++++++++-------- .../modules/landing/landing.component.html | 2 +- grails-app/i18n/messages.properties | 2 +- grails-app/i18n/messages_de.properties | 2 +- grails-app/views/job/_jobTable.gsp | 2 +- 9 files changed, 86 insertions(+), 73 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bc90f31603..b63ef07a6c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -706,9 +706,9 @@ } }, "@ng-select/ng-select": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-2.17.0.tgz", - "integrity": "sha512-hCZIcg4tACYp8tKRLZnD9CEZJcVJbKWkVrS++8dh0cJv580SCiKTQ10CJ1qzWWvmC4tIbCRyLzrUuKQRdkRWpA==", + "version": "2.20.5", + "resolved": "https://registry.npmjs.org/@ng-select/ng-select/-/ng-select-2.20.5.tgz", + "integrity": "sha512-S9R3op3kd8XmAI99exPOTXwDcYOsDngIhkdsQuV1jq/rSVKdJZ6gQY1VWS5REjVnqbKbxbWAx4JA38bum/8Z7g==", "requires": { "tslib": "^1.9.0" } diff --git a/frontend/package.json b/frontend/package.json index 041275e314..bceef5c6db 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,7 +22,7 @@ "@angular/platform-browser": "^7.2.13", "@angular/platform-browser-dynamic": "^7.2.13", "@angular/router": "^7.2.13", - "@ng-select/ng-select": "^2.17.0", + "@ng-select/ng-select": "^2.20.5", "@ngx-translate/core": "^11.0.1", "@ngx-translate/http-loader": "^4.0.0", "core-js": "^3.0.1", diff --git a/frontend/src/app/modules/job-result/job-result.component.html b/frontend/src/app/modules/job-result/job-result.component.html index c02e7ee353..d1ecba3eb6 100644 --- a/frontend/src/app/modules/job-result/job-result.component.html +++ b/frontend/src/app/modules/job-result/job-result.component.html @@ -100,7 +100,7 @@

    aria-hidden="true" class="fas fa-sort-up"> aria-hidden="true" class="fas fa-sort-up"> this.statusService.getJobResultStatusGroupName(jobResultStatus); + compareStatusFn = (item, selected): boolean => this.statusService.compareStatus(item, selected); - wptStatusGroupByFn = (wptStatus: string): string => this.statusService.getWptStatusGroupName(wptStatus); + jobResultStatusGroupByFn = (jobResultStatus: string): string => this.statusService.getJobResultStatusGroupLabel(jobResultStatus); + + wptStatusGroupByFn = (wptStatus: string): string => this.statusService.getWptStatusGroupLabel(wptStatus); groupValueFn = (groupName: string, children: any[]): any => ({label: groupName, children: children}); @@ -57,14 +59,6 @@ export class JobResultComponent implements OnInit { this.getAllJobs(); } - getAllJobs(): void { - this.dataService.getAllJobs() - .subscribe((jobs: Job[]) => { - this.jobs = jobs; - this.readQueryParams(); - }); - } - setJob(job: Job): void { if (job) { this.writeQueryParams(); @@ -129,6 +123,28 @@ export class JobResultComponent implements OnInit { this.applyFilter(); } + isDateTimeRangeSet(dateTimeComponent: DateTimeRange): boolean { + if (dateTimeComponent === DateTimeRange.FROM && this.filter.dateTimeRange[0]) { + return !isNaN(this.filter.dateTimeRange[0].valueOf()); + } + if (dateTimeComponent === DateTimeRange.TO && this.filter.dateTimeRange[1]) { + return !isNaN(this.filter.dateTimeRange[1].valueOf()); + } + return false; + } + + isTestNotTerminated(jobResultStatus: string): boolean { + return this.statusService.isTestNotTerminated(jobResultStatus); + } + + isTestSuccessful(jobResultStatus: string): boolean { + return this.statusService.isTestSuccessful(jobResultStatus); + } + + hasTestFailed(jobResultStatus: string): boolean { + return this.statusService.hasTestFailed(jobResultStatus); + } + observeCalendarEvents(dateTimeComponent: DateTimeRange): void { if (!this.dateTimeRangeFrom || !this.dateTimeRangeTo) { return; @@ -157,28 +173,6 @@ export class JobResultComponent implements OnInit { }); } - isDateTimeRangeSet(dateTimeComponent: DateTimeRange): boolean { - if (dateTimeComponent === DateTimeRange.FROM && this.filter.dateTimeRange[0]) { - return !isNaN(this.filter.dateTimeRange[0].valueOf()); - } - if (dateTimeComponent === DateTimeRange.TO && this.filter.dateTimeRange[1]) { - return !isNaN(this.filter.dateTimeRange[1].valueOf()); - } - return false; - } - - isTestNotTerminated(jobResultStatus: string): boolean { - return this.statusService.isTestNotTerminated(jobResultStatus); - } - - isTestSuccessful(jobResultStatus: string): boolean { - return this.statusService.isTestSuccessful(jobResultStatus); - } - - hasTestFailed(jobResultStatus: string): boolean { - return this.statusService.hasTestFailed(jobResultStatus); - } - private setCalendarLanguage(): void { if (this.osmLangService.getOsmLang() === 'en') { this.dateTimeAdapter.setLocale('en-GB'); @@ -187,6 +181,14 @@ export class JobResultComponent implements OnInit { } } + private getAllJobs(): void { + this.dataService.getAllJobs() + .subscribe((jobs: Job[]) => { + this.jobs = jobs; + this.readQueryParams(); + }); + } + private readQueryParams(): void { this.route.queryParams.subscribe((params: Params) => { if (this.checkQuery(params)) { diff --git a/frontend/src/app/modules/job-result/services/status.service.ts b/frontend/src/app/modules/job-result/services/status.service.ts index 6fd3e1a2b2..d6325c8024 100644 --- a/frontend/src/app/modules/job-result/services/status.service.ts +++ b/frontend/src/app/modules/job-result/services/status.service.ts @@ -55,7 +55,15 @@ export class StatusService { constructor(private translateService: TranslateService) { } - getJobResultStatusGroupName(jobResultStatus: string): string { + compareStatus(item, selected): boolean { + if (selected.label) { + return item.label ? item.label === selected.label : false; + } else { + return item === selected; + } + } + + getJobResultStatusGroupLabel(jobResultStatus: string): string { if (this.isTestNotTerminated(jobResultStatus)) { return this.translateService.instant(StatusGroup.GROUP_NOT_TERMINATED); } @@ -67,7 +75,7 @@ export class StatusService { } } - getWptStatusGroupName(wptStatus: string): string { + getWptStatusGroupLabel(wptStatus: string): string { if (this.isWptNotTerminated(wptStatus)) { return this.translateService.instant(StatusGroup.GROUP_NOT_TERMINATED); } @@ -79,18 +87,18 @@ export class StatusService { } } - writeStatusAsQueryParam(statusList: (string | object)[], enumeration: any, isJobSelected: boolean): string[] { - if (isJobSelected && statusList.length > 0) { - return statusList.map(status => { - if (typeof status === 'string') { + writeStatusAsQueryParam(selectedStatusList: (string | object)[], statusEnum: any, isJobSelected: boolean): string[] { + if (isJobSelected && selectedStatusList.length > 0) { + return selectedStatusList.map(selectedStatus => { + if (typeof selectedStatus === 'string') { return encodeURIComponent( - Object.keys(enumeration).find(key => enumeration[key] === status).toLowerCase() + Object.keys(statusEnum).find(key => statusEnum[key] === selectedStatus).toLowerCase() ); } - if (typeof status === 'object' && status['label'] && typeof status['label'] === 'string') { + if (typeof selectedStatus === 'object' && selectedStatus['label'] && typeof selectedStatus['label'] === 'string') { return encodeURIComponent( Object.keys(StatusGroup).find(key => - this.translateService.instant(StatusGroup[key]) === status['label']).toLowerCase() + this.translateService.instant(StatusGroup[key]) === selectedStatus['label']).toLowerCase() ); } }); @@ -98,32 +106,20 @@ export class StatusService { return null; } - readStatusByQueryParam(statusParam: (string | string[]), - enumeration: any, - statusGroupType: { [key: string]: string [] }): (string | object)[] { - if (!statusParam) { + readStatusByQueryParam(queryParam: (string | string[]), + statusEnum: any, + childrenByStatusGroup: { [key: string]: string [] }): (string | object)[] { + if (!queryParam) { return []; } - if (typeof statusParam === 'string') { - statusParam = decodeURIComponent(statusParam).toUpperCase(); - if (Object.keys(StatusGroup).find(key => key === statusParam)) { - return [].concat({ - label: this.translateService.instant(StatusGroup[statusParam]), - children: statusGroupType[statusParam] - }); - } - return [].concat(enumeration[statusParam]); + if (typeof queryParam === 'string') { + queryParam = decodeURIComponent(queryParam).toUpperCase(); + return [].concat(this.getStatusLabelOrStatusGroup(queryParam, statusEnum, childrenByStatusGroup)); } - if (Array.isArray(statusParam)) { - return statusParam.map((status: string) => { + if (typeof queryParam === 'object' && Array.isArray(queryParam)) { + return queryParam.map((status: string) => { status = decodeURIComponent(status).toUpperCase(); - if (Object.keys(StatusGroup).find(key => key === status)) { - return { - label: this.translateService.instant(StatusGroup[status]), - children: statusGroupType[status] - }; - } - return enumeration[status]; + return this.getStatusLabelOrStatusGroup(status, statusEnum, childrenByStatusGroup); }); } } @@ -163,4 +159,19 @@ export class StatusService { private hasWptFailed(wptStatus: string): boolean { return this.WPT_STATUS_GROUPS.GROUP_FAILED.includes(wptStatus); } + + private getStatusLabelOrStatusGroup(status: string, + statusEnum: any, + childrenByStatusGroup: { [key: string]: string [] }): (string | object) { + if (Object.keys(StatusGroup).find(key => key === status)) { + // status group + return { + label: this.translateService.instant(StatusGroup[status]), + children: childrenByStatusGroup[status] + }; + } else { + // status + return statusEnum[status]; + } + } } diff --git a/frontend/src/app/modules/landing/landing.component.html b/frontend/src/app/modules/landing/landing.component.html index efa22a5ea7..ab8b97f973 100644 --- a/frontend/src/app/modules/landing/landing.component.html +++ b/frontend/src/app/modules/landing/landing.component.html @@ -40,7 +40,7 @@

    {{ 'frontend.de.iteratec.osm.landing.healthIssues' | translate }}

  • - +