diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index a09e0970dd..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"
}
@@ -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/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/app-routing.module.ts b/frontend/src/app/app-routing.module.ts
index 5d3715b7a9..87d4bf4397 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: 'jobResult',
+ loadChildren: './modules/job-result/job-result.module#JobResultModule'
+ },
{
path: '',
loadChildren: './modules/landing/landing.module#LandingModule',
diff --git a/frontend/src/app/enums/url.enum.ts b/frontend/src/app/enums/url.enum.ts
index 38f322e451..0c067ed4b2 100644
--- a/frontend/src/app/enums/url.enum.ts
+++ b/frontend/src/app/enums/url.enum.ts
@@ -10,5 +10,7 @@ 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',
+ 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
new file mode 100644
index 0000000000..d1ecba3eb6
--- /dev/null
+++ b/frontend/src/app/modules/job-result/job-result.component.html
@@ -0,0 +1,178 @@
+
+ {{ 'frontend.de.iteratec.osm.jobResult.title' | translate }}
+
+
+ {{ '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
new file mode 100644
index 0000000000..35525a5697
--- /dev/null
+++ b/frontend/src/app/modules/job-result/job-result.component.scss
@@ -0,0 +1,124 @@
+.description {
+ margin-left: 10px;
+}
+
+.job-selection {
+ margin-bottom: 20px;
+}
+
+.table-head {
+ max-width: 15em;
+}
+
+.sortable {
+ cursor: pointer;
+}
+
+.date-input-container {
+ display: inline-flex;
+ width: 100%;
+}
+
+.date-input-wrapper {
+ width: 100%;
+ margin-right: 10px;
+}
+
+.input-filter {
+ display: inline;
+ font-weight: normal;
+ border: none;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0);
+ height: 30px;
+ margin-top: 5px;
+ margin-left: -12px;
+}
+
+.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::placeholder {
+ font-style: italic;
+}
+
+.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
+}
+
+.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);
+ min-height: 0;
+ margin-top: 5px;
+ margin-left: -10px;
+}
+
+.ng-select.custom .ng-option {
+ font-weight: normal;
+}
+
+.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-select-container .ng-placeholder {
+ font-style: italic;
+}
+
+.job-status-error {
+ color: #EB5E55;
+}
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..9ccb922f31
--- /dev/null
+++ b/frontend/src/app/modules/job-result/job-result.component.spec.ts
@@ -0,0 +1,36 @@
+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;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [JobResultComponent],
+ imports: [
+ SharedMocksModule
+ ],
+ providers: [
+ OsmLangService,
+ GrailsBridgeService
+ ]
+ })
+ .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..10a013c82c
--- /dev/null
+++ b/frontend/src/app/modules/job-result/job-result.component.ts
@@ -0,0 +1,322 @@
+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';
+import {ActivatedRoute, Params, Router} from '@angular/router';
+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'],
+ encapsulation: ViewEncapsulation.None
+})
+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: JobResultFilter = {dateTimeRange: [], testAgent: '', jobResultStatus: [], wptStatus: [], description: ''};
+
+ 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,
+ private statusService: StatusService,
+ private route: ActivatedRoute,
+ private router: Router,
+ private dateTimeAdapter: DateTimeAdapter,
+ private osmLangService: OsmLangService) {
+ }
+
+ compareStatusFn = (item, selected): boolean => this.statusService.compareStatus(item, selected);
+
+ 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});
+
+ ngOnInit() {
+ this.setCalendarLanguage();
+ this.getAllJobs();
+ }
+
+ setJob(job: Job): void {
+ if (job) {
+ this.writeQueryParams();
+ this.getJobResults(job.id);
+ } else {
+ this.writeQueryParams();
+ this.jobResults = [];
+ this.filteredJobResults = [];
+ }
+ }
+
+ 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.filteredJobResults.sort((a: JobResult, b: JobResult) =>
+ this.compareJobResults(a, b, this.currentSortingRule.column, this.currentSortingRule.direction)
+ );
+ }
+
+ applyFilter(): void {
+ this.writeQueryParams();
+ this.filteredJobResults = this.jobResults.filter((jobResult: JobResult) => {
+ if (this.arePropertiesThatAreFilteredEmpty(jobResult)) {
+ return false;
+ }
+ return this.isJobResultIncludingTerms(jobResult);
+ });
+ }
+
+ 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();
+ }
+
+ 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;
+ }
+
+ 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();
+ }
+ }
+ });
+ }
+
+ private setCalendarLanguage(): void {
+ if (this.osmLangService.getOsmLang() === 'en') {
+ this.dateTimeAdapter.setLocale('en-GB');
+ } else {
+ this.dateTimeAdapter.setLocale(this.osmLangService.getOsmLang());
+ }
+ }
+
+ 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)) {
+ const jobId = parseInt(decodeURIComponent(params.job), 10);
+ if (jobId) {
+ this.selectedJob = this.jobs.find((job: Job) => job.id === jobId);
+ 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
+ );
+ 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);
+ }
+ }
+ }).unsubscribe();
+ }
+
+ private writeQueryParams(): void {
+ const params: Params = {
+ 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([], {
+ queryParams: params,
+ replaceUrl: true
+ });
+ }
+
+ private getJobResults(jobId: number): void {
+ this.dataService.getJobResults(jobId)
+ .subscribe((jobResults: JobResult[]) => {
+ this.jobResults = jobResults;
+ this.filteredJobResults = jobResults;
+ this.applyFilter();
+ });
+ }
+
+ 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;
+ }
+
+ private checkQuery(params: Params): boolean {
+ if (params) {
+ return !!params.job;
+ }
+ 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.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;
+ }
+ if (jobResult.jobResultStatus && this.filter.jobResultStatus.length > 0) {
+ if (!this.statusService.isStatusIncludingTerms(jobResult.jobResultStatus, this.filter.jobResultStatus)) {
+ return false;
+ }
+ }
+ if (jobResult.wptStatus && this.filter.wptStatus.length > 0) {
+ if (!this.statusService.isStatusIncludingTerms(jobResult.wptStatus, this.filter.wptStatus)) {
+ return false;
+ }
+ }
+ if (jobResult.description && !jobResult.description.toLowerCase().includes(this.filter.description.toLowerCase())) {
+ return false;
+ }
+ return true;
+ }
+}
+
+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
new file mode 100644
index 0000000000..6cd3b981d2
--- /dev/null
+++ b/frontend/src/app/modules/job-result/job-result.module.ts
@@ -0,0 +1,28 @@
+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';
+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, data: {title: 'frontend.de.iteratec.osm.jobResult.title'}},
+ {path: '**', redirectTo: 'list', pathMatch: 'full'}
+];
+
+@NgModule({
+ declarations: [JobResultComponent],
+ 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
new file mode 100644
index 0000000000..2e9e8e3da3
--- /dev/null
+++ b/frontend/src/app/modules/job-result/models/job-result-filter.model.ts
@@ -0,0 +1,7 @@
+export interface JobResultFilter {
+ dateTimeRange: Date[];
+ testAgent: string;
+ jobResultStatus: (string | object)[];
+ wptStatus: (string | object)[];
+ description: string;
+}
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/job-result.model.ts b/frontend/src/app/modules/job-result/models/job-result.model.ts
new file mode 100644
index 0000000000..eeee45204a
--- /dev/null
+++ b/frontend/src/app/modules/job-result/models/job-result.model.ts
@@ -0,0 +1,8 @@
+export interface JobResult {
+ testUrl: string;
+ date: Date;
+ testAgent: string;
+ jobResultStatus: string;
+ wptStatus: string;
+ description: string;
+}
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..13aa0e4fca
--- /dev/null
+++ b/frontend/src/app/modules/job-result/models/job.model.ts
@@ -0,0 +1,4 @@
+export interface Job {
+ id: number;
+ label: 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
new file mode 100644
index 0000000000..5faa441324
--- /dev/null
+++ b/frontend/src/app/modules/job-result/models/status-group.enum.ts
@@ -0,0 +1,5 @@
+export enum StatusGroup {
+ 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/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/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..7a9be4fa2f
--- /dev/null
+++ b/frontend/src/app/modules/job-result/services/job-result-data.service.spec.ts
@@ -0,0 +1,15 @@
+import {TestBed} from '@angular/core/testing';
+
+import {JobResultDataService} from './job-result-data.service';
+import {HttpClientTestingModule} from '@angular/common/http/testing';
+
+describe('JobResultDataService', () => {
+ beforeEach(() => TestBed.configureTestingModule({
+ imports: [HttpClientTestingModule]
+ }));
+
+ 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..fc37899a47
--- /dev/null
+++ b/frontend/src/app/modules/job-result/services/job-result-data.service.ts
@@ -0,0 +1,51 @@
+import {Injectable} from '@angular/core';
+import {HttpClient} from '@angular/common/http';
+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'
+})
+export class JobResultDataService {
+
+ constructor(private http: HttpClient) {
+ }
+
+ getAllJobs(): Observable {
+ return this.http.get(URL.ALL_JOBS)
+ .pipe(
+ 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', []))
+ );
+ }
+
+ 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', []))
+ );
+ }
+
+ private handleError(operation = 'operation', result?: T) {
+ return (error: any): Observable => {
+ console.error(operation, error.message);
+ return of(result as T);
+ };
+ }
+}
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..92599aa471
--- /dev/null
+++ b/frontend/src/app/modules/job-result/services/status.service.spec.ts
@@ -0,0 +1,15 @@
+import {TestBed} from '@angular/core/testing';
+
+import {StatusService} from './status.service';
+import {SharedMocksModule} from '../../../testing/shared-mocks.module';
+
+describe('StatusService', () => {
+ beforeEach(() => TestBed.configureTestingModule({
+ imports: [SharedMocksModule]
+ }));
+
+ 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..d6325c8024
--- /dev/null
+++ b/frontend/src/app/modules/job-result/services/status.service.ts
@@ -0,0 +1,177 @@
+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) {
+ }
+
+ 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);
+ }
+ if (this.isTestSuccessful(jobResultStatus)) {
+ return this.translateService.instant(StatusGroup.GROUP_SUCCESS);
+ }
+ if (this.hasTestFailed(jobResultStatus)) {
+ return this.translateService.instant(StatusGroup.GROUP_FAILED);
+ }
+ }
+
+ getWptStatusGroupLabel(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(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(statusEnum).find(key => statusEnum[key] === selectedStatus).toLowerCase()
+ );
+ }
+ if (typeof selectedStatus === 'object' && selectedStatus['label'] && typeof selectedStatus['label'] === 'string') {
+ return encodeURIComponent(
+ Object.keys(StatusGroup).find(key =>
+ this.translateService.instant(StatusGroup[key]) === selectedStatus['label']).toLowerCase()
+ );
+ }
+ });
+ }
+ return null;
+ }
+
+ readStatusByQueryParam(queryParam: (string | string[]),
+ statusEnum: any,
+ childrenByStatusGroup: { [key: string]: string [] }): (string | object)[] {
+ if (!queryParam) {
+ return [];
+ }
+ if (typeof queryParam === 'string') {
+ queryParam = decodeURIComponent(queryParam).toUpperCase();
+ return [].concat(this.getStatusLabelOrStatusGroup(queryParam, statusEnum, childrenByStatusGroup));
+ }
+ if (typeof queryParam === 'object' && Array.isArray(queryParam)) {
+ return queryParam.map((status: string) => {
+ status = decodeURIComponent(status).toUpperCase();
+ return this.getStatusLabelOrStatusGroup(status, statusEnum, childrenByStatusGroup);
+ });
+ }
+ }
+
+ 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);
+ }
+
+ 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/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/frontend/src/app/modules/landing/landing.component.html b/frontend/src/app/modules/landing/landing.component.html
index e57523b3f6..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 }}
20"
[class.warning]="failingJob.percentageFailLast5 <= 20">
-
+
-
-
-
-
-
+
+
+
+
+
diff --git a/grails-app/views/job/_jobTable.gsp b/grails-app/views/job/_jobTable.gsp
index 58b3ab8814..6ec0227c84 100644
--- a/grails-app/views/job/_jobTable.gsp
+++ b/grails-app/views/job/_jobTable.gsp
@@ -55,7 +55,7 @@
-
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()