1/** 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16import { Component, OnInit, ViewChild } from '@angular/core'; 17import { MatSnackBar, MatTableDataSource, PageEvent } from '@angular/material'; 18import { animate, state, style, transition, trigger } from '@angular/animations'; 19 20import { AppService } from '../../appservice'; 21import { FilterComponent } from '../../shared/filter/filter.component'; 22import { FilterCondition } from '../../model/filter_condition'; 23import { FilterItem } from '../../model/filter_item'; 24import { MenuBaseClass } from '../menu_base'; 25import { Job } from '../../model/job'; 26import { JobService } from './job.service'; 27import { JobStatus, TestType } from '../../shared/vtslab_status'; 28 29import * as moment from 'moment-timezone'; 30 31 32/** Component that handles job menu. */ 33@Component({ 34 selector: 'app-job', 35 templateUrl: './job.component.html', 36 providers: [ JobService ], 37 styleUrls: ['./job.component.scss'], 38 animations: [ 39 trigger('detailExpand', [ 40 state('void', style({height: '0px', minHeight: '0', visibility: 'hidden'})), 41 state('*', style({height: '*', visibility: 'visible'})), 42 transition('void <=> *', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')), 43 ]), 44 ], 45}) 46export class JobComponent extends MenuBaseClass implements OnInit { 47 columnTitles = [ 48 '_index', 49 'test_type', 50 'test_name', 51 'hostname', 52 'device', 53 'serial', 54 'manifest_branch', 55 'build_target', 56 'build_id', 57 'gsi_branch', 58 'gsi_build_target', 59 'gsi_build_id', 60 'test_branch', 61 'test_build_target', 62 'test_build_id', 63 'status', 64 'timestamp', 65 'heartbeat_stamp', 66 ]; 67 statColumnTitles = [ 68 'hours', 69 'created', 70 'completed', 71 'running', 72 'bootup_err', 73 'infra_err', 74 'expired', 75 ]; 76 dataSource = new MatTableDataSource<Job>(); 77 statDataSource = new MatTableDataSource(); 78 pageEvent: PageEvent; 79 jobStatusEnum = JobStatus; 80 appliedFilters: FilterItem[]; 81 82 @ViewChild(FilterComponent) filterComponent: FilterComponent; 83 84 sort = ''; 85 sortDirection = ''; 86 87 constructor(private jobService: JobService, 88 appService: AppService, 89 snackBar: MatSnackBar) { 90 super(appService, snackBar); 91 } 92 93 ngOnInit(): void { 94 // By default, job page requires list in desc order by timestamp. 95 this.sort = 'timestamp'; 96 this.sortDirection = 'desc'; 97 98 this.filterComponent.setSelectorList(Job); 99 this.getCount(); 100 this.getStatistics(); 101 this.getJobs(this.pageSize, this.pageSize * this.pageIndex); 102 } 103 104 /** Gets a total count of jobs. */ 105 getCount(observer = this.getDefaultCountObservable()) { 106 const filterJSON = (this.appliedFilters) ? JSON.stringify(this.appliedFilters) : ''; 107 this.jobService.getCount(filterJSON).subscribe(observer); 108 } 109 110 /** Gets jobs. 111 * @param size A number, at most this many results will be returned. 112 * @param offset A Number of results to skip. 113 */ 114 getJobs(size = 0, offset = 0) { 115 this.loading = true; 116 const filterJSON = (this.appliedFilters) ? JSON.stringify(this.appliedFilters) : ''; 117 this.jobService.getJobs(size, offset, filterJSON, this.sort, this.sortDirection) 118 .subscribe( 119 (response) => { 120 this.loading = false; 121 if (this.count >= 0) { 122 let length = 0; 123 if (response.jobs) { 124 length = response.jobs.length; 125 } 126 const total = length + offset; 127 if (response.has_next) { 128 if (length !== this.pageSize) { 129 this.showSnackbar('Received unexpected number of entities.'); 130 } else if (this.count <= total) { 131 this.getCount(); 132 } 133 } else { 134 if (this.count !== total) { 135 if (length !== this.count) { 136 this.getCount(); 137 } else if (this.count > total) { 138 const countObservable = this.getDefaultCountObservable([ 139 () => { 140 this.pageIndex = Math.floor(this.count / this.pageSize); 141 this.getJobs(this.pageSize, this.pageSize * this.pageIndex); 142 } 143 ]); 144 this.getCount(countObservable); 145 } 146 } 147 } 148 } 149 this.dataSource.data = response.jobs; 150 }, 151 (error) => this.showSnackbar(`[${error.status}] ${error.name}`) 152 ); 153 } 154 155 /** Hooks a page event and handles properly. */ 156 onPageEvent(event: PageEvent) { 157 this.pageSize = event.pageSize; 158 this.pageIndex = event.pageIndex; 159 this.getJobs(this.pageSize, this.pageSize * this.pageIndex); 160 return event; 161 } 162 163 /** Gets the recent jobs and calculate statistics */ 164 getStatistics() { 165 const timeFilter = new FilterItem(); 166 timeFilter.key = 'timestamp'; 167 timeFilter.method = FilterCondition.GreaterThan; 168 timeFilter.value = '72'; 169 const timeFilterString = JSON.stringify([timeFilter]); 170 this.jobService.getJobs(0, 0, timeFilterString, '', '') 171 .subscribe( 172 (response) => { 173 const stats_72hrs = this.buildStatisticsData('72 Hours', response.jobs); 174 const jobs_24hrs = (response.jobs == null || response.jobs.length === 0) ? undefined : response.jobs.filter( 175 job => (moment() - moment.tz(job.timestamp, 'YYYY-MM-DDThh:mm:ss', 'UTC')) / 3600000 < 24); 176 const stats_24hrs = this.buildStatisticsData('24 Hours', jobs_24hrs); 177 this.statDataSource.data = [stats_24hrs, stats_72hrs]; 178 }, 179 (error) => this.showSnackbar(`[${error.status}] ${error.name}`) 180 ); 181 } 182 183 /** Builds statistics from given jobs list */ 184 buildStatisticsData(title, jobs) { 185 if (jobs == null || jobs.length === 0) { 186 return { hours: title, created: 0, completed: 0, running: 0, bootup_err: 0, infra_err: 0, expired: 0 }; 187 } 188 return { 189 hours: title, 190 created: jobs.length, 191 completed: jobs.filter(job => job.status != null && Number(job.status) === JobStatus.Complete).length, 192 running: jobs.filter(job => job.status != null && 193 (Number(job.status) === JobStatus.Leased || Number(job.status) === JobStatus.Ready)).length, 194 bootup_err: jobs.filter(job => job.status != null && Number(job.status) === JobStatus.Bootup_err).length, 195 infra_err: jobs.filter(job => job.status != null && Number(job.status) === JobStatus.Infra_err).length, 196 expired: jobs.filter(job => job.status != null && Number(job.status) === JobStatus.Expired).length, 197 }; 198 } 199 200 /** Generates text to represent in HTML with given test type. */ 201 getTestTypeText(status: number) { 202 if (status === undefined || status & TestType.Unknown) { 203 return TestType[TestType.Unknown]; 204 } 205 206 const text_list = []; 207 [TestType.ToT, TestType.OTA, TestType.Signed, TestType.Manual].forEach(function (value) { 208 if (status & value) { text_list.push(TestType[value]); } 209 }); 210 211 return text_list.join(', '); 212 } 213 214 /** Applies a filter and get entities with it. */ 215 applyFilters(filters) { 216 this.pageIndex = 0; 217 this.appliedFilters = filters; 218 this.getCount(); 219 this.getJobs(this.pageSize, this.pageSize * this.pageIndex); 220 } 221} 222