summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJongmok Hong <jongmok@google.com>2018-09-05 19:03:00 +0900
committerJongmok Hong <jongmok@google.com>2018-09-06 17:09:24 +0900
commit0f4a2420cdda7a1e9da352522eb267bdb620913a (patch)
tree491286a79a619988d2e214c3186a74cb6e0ab762
parentff618fff8cf97e0e5431eaa1382b5f16b7ac469c (diff)
downloadtest_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.html41
-rw-r--r--gae/frontend/src/app/menu/job/job.component.ts50
-rw-r--r--gae/frontend/src/app/model/filter_condition.ts24
-rw-r--r--gae/frontend/src/app/model/filter_item.ts22
-rw-r--r--gae/frontend/src/app/shared/vtslab_status.ts23
-rw-r--r--gae/frontend/src/styles.scss8
-rw-r--r--gae/webapp/src/endpoint/endpoint_base.py14
-rw-r--r--gae/webapp/src/proto/model.py4
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."""