summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJongmok Hong <jongmok@google.com>2018-09-27 06:04:10 +0900
committerJongmok Hong <jongmok@google.com>2018-09-27 07:39:50 +0900
commit4c2607629d3de9c03005710496a12cd84a7467d0 (patch)
tree696341faaef0f899f879663fed613fb1df1eda1a
parent086619542c2b7bb3b5879f6ac1c7bcf15aa0f819 (diff)
downloadtest_serving-4c2607629d3de9c03005710496a12cd84a7467d0.tar.gz
Add a schedule suspend feature in extended rows.
Test: go/vtslab-schedule-dev/redirect/20180927t073313-dot-vtslab-schedule-dev Bug: 116058216
-rw-r--r--gae/frontend/src/app/menu/schedule/schedule.component.html12
-rw-r--r--gae/frontend/src/app/menu/schedule/schedule.component.ts29
-rw-r--r--gae/frontend/src/app/menu/schedule/schedule.service.ts7
-rw-r--r--gae/frontend/src/app/model/schedule.ts12
-rw-r--r--gae/webapp/src/endpoint/endpoint_base.py2
-rw-r--r--gae/webapp/src/endpoint/schedule_info.py30
-rw-r--r--gae/webapp/src/proto/model.py16
7 files changed, 105 insertions, 3 deletions
diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.html b/gae/frontend/src/app/menu/schedule/schedule.component.html
index 67088fc..c567639 100644
--- a/gae/frontend/src/app/menu/schedule/schedule.component.html
+++ b/gae/frontend/src/app/menu/schedule/schedule.component.html
@@ -84,7 +84,10 @@
</ng-container>
<mat-header-row *matHeaderRowDef="columnTitles"></mat-header-row>
- <mat-row *matRowDef="let row; columns: columnTitles;"></mat-row>
+ <mat-row *matRowDef="let row; columns: columnTitles;"
+ matRipple
+ class="element-row"
+ [appCdkDetailRow]="row" [appCdkDetailRowTpl]="schedule_detail"></mat-row>
</mat-table>
<mat-paginator [length]="count"
@@ -94,6 +97,13 @@
(page)="pageEvent = onPageEvent($event)">
</mat-paginator>
</div>
+<ng-template #schedule_detail let-schedule>
+ <div class="mat-row div-expandable" [@detailExpand] style="overflow: hidden">
+ <button mat-raised-button (click)="suspendSchedule([{urlsafe_key: schedule.urlsafe_key, suspend: !schedule.suspended}])">
+ {{(schedule.suspended ? "Resume" : "Suspend")}}
+ </button>
+ </div>
+</ng-template>
<div class="loading-spinner" *ngIf="loading">
<mat-spinner color="primary"></mat-spinner>
</div>
diff --git a/gae/frontend/src/app/menu/schedule/schedule.component.ts b/gae/frontend/src/app/menu/schedule/schedule.component.ts
index 61f98fd..57ebd7b 100644
--- a/gae/frontend/src/app/menu/schedule/schedule.component.ts
+++ b/gae/frontend/src/app/menu/schedule/schedule.component.ts
@@ -15,11 +15,12 @@
*/
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatSnackBar, MatTableDataSource, PageEvent } from '@angular/material';
+import { animate, state, style, transition, trigger } from "@angular/animations";
import { FilterComponent } from '../../shared/filter/filter.component';
import { FilterItem } from '../../model/filter_item';
import { MenuBaseClass } from '../menu_base';
-import { Schedule } from '../../model/schedule';
+import { Schedule, ScheduleSuspendResponse } from '../../model/schedule';
import { ScheduleService } from './schedule.service';
@@ -29,6 +30,13 @@ import { ScheduleService } from './schedule.service';
templateUrl: './schedule.component.html',
providers: [ ScheduleService ],
styleUrls: ['./schedule.component.scss'],
+ animations: [
+ trigger('detailExpand', [
+ state('void', style({height: '0px', minHeight: '0', visibility: 'hidden'})),
+ state('*', style({height: '*', visibility: 'visible'})),
+ transition('void <=> *', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
+ ]),
+ ],
})
export class ScheduleComponent extends MenuBaseClass implements OnInit {
columnTitles = [
@@ -112,6 +120,25 @@ export class ScheduleComponent extends MenuBaseClass implements OnInit {
);
}
+ /** Toggles a schedule from suspend to resume, or vice versa. */
+ suspendSchedule(schedules: ScheduleSuspendResponse[]) {
+ this.scheduleService.suspendSchedule(schedules)
+ .subscribe(
+ (response) => {
+ if (response.schedules) {
+ let self = this;
+ response.schedules.forEach(function(schedule) {
+ const original = self.dataSource.data.filter(x => x.urlsafe_key === schedule.urlsafe_key);
+ if (original) {
+ original[0].suspended = schedule.suspend;
+ }
+ })
+ }
+ },
+ (error) => this.showSnackbar(`[${error.status}] ${error.name}`)
+ );
+ }
+
/** Hooks a page event and handles properly. */
onPageEvent(event: PageEvent) {
this.pageSize = event.pageSize;
diff --git a/gae/frontend/src/app/menu/schedule/schedule.service.ts b/gae/frontend/src/app/menu/schedule/schedule.service.ts
index 86c831d..ae534dd 100644
--- a/gae/frontend/src/app/menu/schedule/schedule.service.ts
+++ b/gae/frontend/src/app/menu/schedule/schedule.service.ts
@@ -22,6 +22,7 @@ import { Observable } from 'rxjs';
import { environment } from '../../../environments/environment';
import { ScheduleWrapper } from '../../model/schedule_wrapper';
import { ServiceBase } from '../../shared/servicebase';
+import { ScheduleSuspendResponse, ScheduleSuspendResponseWrapper } from '../../model/schedule';
@Injectable()
@@ -41,4 +42,10 @@ export class ScheduleService extends ServiceBase {
return this.httpClient.post<ScheduleWrapper>(url, {size: size, offset: offset, filter: filterInfo, sort: sort, direction: direction})
.pipe(catchError(this.handleError));
}
+
+ suspendSchedule(schedules: ScheduleSuspendResponse[]): Observable<ScheduleSuspendResponseWrapper> {
+ const url = this.url + 'suspend';
+ return this.httpClient.post<ScheduleSuspendResponseWrapper>(url, {schedules: schedules})
+ .pipe(catchError(this.handleError));
+ }
}
diff --git a/gae/frontend/src/app/model/schedule.ts b/gae/frontend/src/app/model/schedule.ts
index 756cb00..5046f2f 100644
--- a/gae/frontend/src/app/model/schedule.ts
+++ b/gae/frontend/src/app/model/schedule.ts
@@ -59,4 +59,16 @@ export class Schedule {
image_package_repo_base: string = void 0;
timestamp = void 0;
owner: string[] = void 0;
+
+ suspended: boolean = void 0;
+ urlsafe_key: string = void 0;
+}
+
+export interface ScheduleSuspendResponseWrapper {
+ schedules: ScheduleSuspendResponse[];
+}
+
+export interface ScheduleSuspendResponse {
+ urlsafe_key: string;
+ suspend: boolean;
}
diff --git a/gae/webapp/src/endpoint/endpoint_base.py b/gae/webapp/src/endpoint/endpoint_base.py
index 0e429dd..d0dddd5 100644
--- a/gae/webapp/src/endpoint/endpoint_base.py
+++ b/gae/webapp/src/endpoint/endpoint_base.py
@@ -323,6 +323,8 @@ class EndpointBase(remote.Service):
resource=entity, reference=message)
for attr in assigned_attributes:
entity_dict[attr] = getattr(entity, attr, None)
+ if hasattr(message, "urlsafe_key"):
+ entity_dict["urlsafe_key"] = entity.key.urlsafe()
return_list.append(entity_dict)
return return_list, more
diff --git a/gae/webapp/src/endpoint/schedule_info.py b/gae/webapp/src/endpoint/schedule_info.py
index ce13a40..e353902 100644
--- a/gae/webapp/src/endpoint/schedule_info.py
+++ b/gae/webapp/src/endpoint/schedule_info.py
@@ -21,8 +21,11 @@ from google.appengine.ext import ndb
from webapp.src import vtslab_status as Status
from webapp.src.endpoint import endpoint_base
from webapp.src.proto import model
+from webapp.src.utils import email_util
SCHEDULE_INFO_RESOURCE = endpoints.ResourceContainer(model.ScheduleInfoMessage)
+SCHEDULE_SUSPEND_RESOURCE = endpoints.ResourceContainer(
+ model.ScheduleSuspendMessage)
@endpoints.api(name="schedule", version="v1")
@@ -134,6 +137,33 @@ class ScheduleInfoApi(endpoint_base.EndpointBase):
return model.CountResponseMessage(count=count)
+ @endpoints.method(
+ SCHEDULE_SUSPEND_RESOURCE,
+ model.ScheduleSuspendMessage,
+ path="suspend",
+ http_method="POST",
+ name="suspend")
+ def suspend(self, request):
+ """Toggles a schedule from suspend to resume, or vice versa."""
+ schedules_to_put = []
+ schedules_to_return = []
+ for schedule in request.schedules:
+ schedule_key = ndb.key.Key(urlsafe=schedule.urlsafe_key)
+ schedule_entity = schedule_key.get()
+ if schedule.suspend: # to suspend
+ schedule_entity.suspended = True
+ else: # to resume
+ schedule_entity.error_count = 0
+ schedule_entity.suspended = False
+ schedules_to_put.append(schedule_entity)
+ schedules_to_return.append({"urlsafe_key": schedule.urlsafe_key,
+ "suspend": schedule_entity.suspended})
+ # TODO(jongmok): Minimize a number of emails by merging schedules.
+ email_util.send_schedule_suspension_notification(schedule_entity)
+
+ ndb.put_multi(schedules_to_put)
+ return model.ScheduleSuspendMessage(schedules=schedules_to_return)
+
@endpoints.api(name="green_schedule_info", version="v1")
class GreenScheduleInfoApi(endpoint_base.EndpointBase):
diff --git a/gae/webapp/src/proto/model.py b/gae/webapp/src/proto/model.py
index 52b7a8a..56b4566 100644
--- a/gae/webapp/src/proto/model.py
+++ b/gae/webapp/src/proto/model.py
@@ -114,7 +114,7 @@ class ScheduleControlInfoMessage(messages.Message):
class ScheduleInfoMessage(messages.Message):
"""A message for representing an individual schedule entry."""
- # Next ID = 36
+ # Next ID = 38
# schedule name for green build schedule, optional.
name = messages.StringField(16)
schedule_type = messages.StringField(19)
@@ -162,6 +162,9 @@ class ScheduleInfoMessage(messages.Message):
timestamp = message_types.DateTimeField(34)
owner = messages.StringField(35, repeated=True)
+ suspended = messages.BooleanField(36)
+ urlsafe_key = messages.StringField(37)
+
class LabModel(ndb.Model):
"""A model for representing an individual lab entry."""
@@ -428,3 +431,14 @@ class CountRequestMessage(messages.Message):
class CountResponseMessage(messages.Message):
"""A message of a count of entities to respond to /count endpoints."""
count = messages.IntegerField(1)
+
+
+class ScheduleSuspendMessage(messages.Message):
+ """A response message to schedule endpoint API's /suspend method."""
+
+ class SingleScheduleSuspendMessage(messages.Message):
+ urlsafe_key = messages.StringField(1)
+ suspend = messages.BooleanField(2)
+
+ schedules = messages.MessageField(
+ SingleScheduleSuspendMessage, 1, repeated=True)