diff options
author | Jongmok Hong <jongmok@google.com> | 2018-09-05 19:03:00 +0900 |
---|---|---|
committer | Jongmok Hong <jongmok@google.com> | 2018-09-06 17:09:24 +0900 |
commit | 0f4a2420cdda7a1e9da352522eb267bdb620913a (patch) | |
tree | 491286a79a619988d2e214c3186a74cb6e0ab762 | |
parent | ff618fff8cf97e0e5431eaa1382b5f16b7ac469c (diff) | |
download | test_serving-0f4a2420cdda7a1e9da352522eb267bdb620913a.tar.gz |
Add statistics table in job page.
Test: go/vtslab-schedule-dev
Bug: 74575555
Change-Id: I8156f1e77608ee032c834e9c466392b2f36d2b89
-rw-r--r-- | gae/frontend/src/app/menu/job/job.component.html | 41 | ||||
-rw-r--r-- | gae/frontend/src/app/menu/job/job.component.ts | 50 | ||||
-rw-r--r-- | gae/frontend/src/app/model/filter_condition.ts | 24 | ||||
-rw-r--r-- | gae/frontend/src/app/model/filter_item.ts | 22 | ||||
-rw-r--r-- | gae/frontend/src/app/shared/vtslab_status.ts | 23 | ||||
-rw-r--r-- | gae/frontend/src/styles.scss | 8 | ||||
-rw-r--r-- | gae/webapp/src/endpoint/endpoint_base.py | 14 | ||||
-rw-r--r-- | gae/webapp/src/proto/model.py | 4 |
8 files changed, 185 insertions, 1 deletions
diff --git a/gae/frontend/src/app/menu/job/job.component.html b/gae/frontend/src/app/menu/job/job.component.html index d8dbcca..3628464 100644 --- a/gae/frontend/src/app/menu/job/job.component.html +++ b/gae/frontend/src/app/menu/job/job.component.html @@ -12,6 +12,47 @@ See the License for the specific language governing permissions and limitations under the License. --> +<div class="statistics-table" [ngStyle]="{'opacity': (loading) ? 0.2 : 1 }"> + <mat-table [dataSource]="statDataSource"> + <ng-container matColumnDef="hours"> + <mat-header-cell *matHeaderCellDef>Stats</mat-header-cell> + <mat-cell *matCellDef="let stat"> {{stat.hours}} </mat-cell> + </ng-container> + + <ng-container matColumnDef="created"> + <mat-header-cell *matHeaderCellDef>Created</mat-header-cell> + <mat-cell *matCellDef="let stat"> {{stat.created}} </mat-cell> + </ng-container> + + <ng-container matColumnDef="completed"> + <mat-header-cell *matHeaderCellDef>Completed</mat-header-cell> + <mat-cell *matCellDef="let stat"> {{stat.completed}} ({{stat.created > 0 ? stat.completed/stat.created*100 : 0}})% </mat-cell> + </ng-container> + + <ng-container matColumnDef="running"> + <mat-header-cell *matHeaderCellDef>Running/Ready</mat-header-cell> + <mat-cell *matCellDef="let stat"> {{stat.running}} ({{stat.created > 0 ? stat.running/stat.created*100 : 0}})% </mat-cell> + </ng-container> + + <ng-container matColumnDef="bootup_err"> + <mat-header-cell *matHeaderCellDef>Boot-up Error</mat-header-cell> + <mat-cell *matCellDef="let stat"> {{stat.bootup_err}} ({{stat.created > 0 ? stat.bootup_err/stat.created*100 : 0}})% </mat-cell> + </ng-container> + + <ng-container matColumnDef="infra_err"> + <mat-header-cell *matHeaderCellDef>Infra Error</mat-header-cell> + <mat-cell *matCellDef="let stat"> {{stat.infra_err}} ({{stat.created > 0 ? stat.infra_err/stat.created*100 : 0}})% </mat-cell> + </ng-container> + + <ng-container matColumnDef="expired"> + <mat-header-cell *matHeaderCellDef>Expired</mat-header-cell> + <mat-cell *matCellDef="let stat"> {{stat.expired}} ({{stat.created > 0 ? stat.expired/stat.created*100 : 0}})% </mat-cell> + </ng-container> + + <mat-header-row *matHeaderRowDef="statColumnTitles"></mat-header-row> + <mat-row *matRowDef="let row; columns: statColumnTitles;"></mat-row> + </mat-table> +</div> <div class="mat-elevation-z2 entity-table" [ngStyle]="{'opacity': (loading) ? 0.2 : 1 }"> <table mat-table [dataSource]="dataSource"> <!-- Index Column --> diff --git a/gae/frontend/src/app/menu/job/job.component.ts b/gae/frontend/src/app/menu/job/job.component.ts index d3caccf..c1a31bb 100644 --- a/gae/frontend/src/app/menu/job/job.component.ts +++ b/gae/frontend/src/app/menu/job/job.component.ts @@ -16,9 +16,14 @@ import { Component, OnInit } from '@angular/core'; import { MatTableDataSource, PageEvent } from '@angular/material'; +import { FilterCondition } from '../../model/filter_condition'; +import { FilterItem } from '../../model/filter_item'; import { MenuBaseClass } from '../menu_base'; import { Job } from '../../model/job'; import { JobService } from './job.service'; +import { JobStatus } from '../../shared/vtslab_status'; + +import * as moment from 'moment-timezone'; /** Component that handles job menu. */ @Component({ @@ -48,7 +53,17 @@ export class JobComponent extends MenuBaseClass implements OnInit { 'test_type', 'timestamp', ]; + statColumnTitles = [ + 'hours', + 'created', + 'completed', + 'running', + 'bootup_err', + 'infra_err', + 'expired', + ]; dataSource = new MatTableDataSource<Job>(); + statDataSource = new MatTableDataSource(); pageEvent: PageEvent; constructor(private jobService: JobService) { @@ -57,6 +72,7 @@ export class JobComponent extends MenuBaseClass implements OnInit { ngOnInit(): void { this.getCount(); + this.getStatistics(); this.getJobs(this.pageSize, this.pageSize * this.pageIndex); } @@ -118,4 +134,38 @@ export class JobComponent extends MenuBaseClass implements OnInit { this.getJobs(this.pageSize, this.pageSize * this.pageIndex); return event; } + + /** Gets the recent jobs and calculate statistics */ + getStatistics() { + const timeFilter = new FilterItem(); + timeFilter.key = 'timestamp'; + timeFilter.method = FilterCondition.GreaterThan; + timeFilter.value = '72'; + const timeFilterString = JSON.stringify([timeFilter]); + this.jobService.getJobs(0, 0, timeFilterString, '', '') + .subscribe( + (response) => { + const stats_72hrs = this.buildStatisticsData('72 Hours', response.jobs); + const jobs_24hrs = response.jobs.filter( + job => (moment() - moment.tz(job.timestamp, 'YYYY-MM-DDThh:mm:ss', 'UTC')) / 3600000 < 24); + const stats_24hrs = this.buildStatisticsData('24 Hours', jobs_24hrs); + this.statDataSource.data = [stats_24hrs, stats_72hrs]; + }, + (error) => console.log(`[${error.status}] ${error.name}`) + ); + } + + /** Builds statistics from given jobs list */ + buildStatisticsData(title, jobs) { + return { + hours: title, + created: jobs.length, + completed: jobs.filter(job => job.status != null && Number(job.status) === JobStatus.complete).length, + running: jobs.filter(job => job.status != null && + (Number(job.status) === JobStatus.leased || Number(job.status) === JobStatus.ready)).length, + bootup_err: jobs.filter(job => job.status != null && Number(job.status) === JobStatus.bootup_err).length, + infra_err: jobs.filter(job => job.status != null && Number(job.status) === JobStatus.infra_err).length, + expired: jobs.filter(job => job.status != null && Number(job.status) === JobStatus.expired).length, + }; + } } diff --git a/gae/frontend/src/app/model/filter_condition.ts b/gae/frontend/src/app/model/filter_condition.ts new file mode 100644 index 0000000..9f76de9 --- /dev/null +++ b/gae/frontend/src/app/model/filter_condition.ts @@ -0,0 +1,24 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export enum FilterCondition { + EqualTo = 1, + LessThan, + GreaterThan, + LessThanOrEqualTo, + GreaterThanOrEqualTo, + NotEqualTo, + Has, +} diff --git a/gae/frontend/src/app/model/filter_item.ts b/gae/frontend/src/app/model/filter_item.ts new file mode 100644 index 0000000..de457a1 --- /dev/null +++ b/gae/frontend/src/app/model/filter_item.ts @@ -0,0 +1,22 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {FilterCondition} from './filter_condition'; + +export class FilterItem { + key: string; + method: FilterCondition; + value: string; // back-end should handle type-casting. +} diff --git a/gae/frontend/src/app/shared/vtslab_status.ts b/gae/frontend/src/app/shared/vtslab_status.ts new file mode 100644 index 0000000..5f063ed --- /dev/null +++ b/gae/frontend/src/app/shared/vtslab_status.ts @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export class JobStatus { + static readonly ready = 0; + static readonly leased = 1; + static readonly complete = 2; + static readonly infra_err = 3; + static readonly expired = 4; + static readonly bootup_err = 5; +} diff --git a/gae/frontend/src/styles.scss b/gae/frontend/src/styles.scss index aabb7b0..61e8933 100644 --- a/gae/frontend/src/styles.scss +++ b/gae/frontend/src/styles.scss @@ -14,6 +14,14 @@ body { } } +.statistics-table { + margin: 10px 20px 20px 20px; + + table { + width: 100%; + } +} + .entity-table { margin: 10px 20px 20px 20px; diff --git a/gae/webapp/src/endpoint/endpoint_base.py b/gae/webapp/src/endpoint/endpoint_base.py index acf13ae..0e429dd 100644 --- a/gae/webapp/src/endpoint/endpoint_base.py +++ b/gae/webapp/src/endpoint/endpoint_base.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import datetime import inspect import logging import json @@ -203,6 +204,19 @@ class EndpointBase(remote.Service): else: logging.debug("Empty repeated list cannot be queried.") empty_repeated_field.append(value) + elif isinstance(metaclass._properties[property_key], + ndb.DateTimeProperty): + if method == Status.FILTER_METHOD[Status.FILTER_LessThan]: + query = query.filter( + getattr(metaclass, property_key) < datetime.datetime. + now() - datetime.timedelta(hours=int(value))) + elif method == Status.FILTER_METHOD[Status.FILTER_GreaterThan]: + query = query.filter( + getattr(metaclass, property_key) > datetime.datetime. + now() - datetime.timedelta(hours=int(value))) + else: + logging.debug("DateTimeProperty only allows <=(less than) " + "and >=(greater than) operation.") else: if method == Status.FILTER_METHOD[Status.FILTER_EqualTo]: query = query.filter( diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py index 25af6dc..352ee56 100644 --- a/gae/webapp/src/proto/model.py +++ b/gae/webapp/src/proto/model.py @@ -297,7 +297,7 @@ class JobModel(ndb.Model): class JobMessage(messages.Message): """A message for representing an individual job entry.""" - # Next ID = 35 + # Next ID = 38 test_type = messages.IntegerField(29) hostname = messages.StringField(1) @@ -347,6 +347,8 @@ class JobMessage(messages.Message): report_persistent_url = messages.StringField(35, repeated=True) report_reference_url = messages.StringField(36, repeated=True) + timestamp = message_types.DateTimeField(37) + class ReturnCodeMessage(messages.Enum): """Enum for default return code.""" |