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 }}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + {{ 'frontend.de.iteratec.osm.jobResult.tableHead.date' | 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 | date:('frontend.de.iteratec.osm.jobResult.dateFormat' | translate) }}
+
+ +
{{ test.testAgent ? test.testAgent : '-' }}{{ 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 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 }}

  • - +
  • -
  • - - - - +
  • + + + +
  • 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()