/** * 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 { Component, OnInit, ViewChild } from '@angular/core'; import { MatSnackBar, MatTableDataSource, PageEvent } from '@angular/material'; import { animate, state, style, transition, trigger } from '@angular/animations'; import { AppService } from '../../appservice'; import { FilterComponent } from '../../shared/filter/filter.component'; 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, TestType } from '../../shared/vtslab_status'; import * as moment from 'moment-timezone'; /** Component that handles job menu. */ @Component({ selector: 'app-job', templateUrl: './job.component.html', providers: [ JobService ], styleUrls: ['./job.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 JobComponent extends MenuBaseClass implements OnInit { columnTitles = [ '_index', 'test_type', 'test_name', 'hostname', 'device', 'serial', 'manifest_branch', 'build_target', 'build_id', 'gsi_branch', 'gsi_build_target', 'gsi_build_id', 'test_branch', 'test_build_target', 'test_build_id', 'status', 'timestamp', 'heartbeat_stamp', ]; statColumnTitles = [ 'hours', 'created', 'completed', 'running', 'bootup_err', 'infra_err', 'expired', ]; dataSource = new MatTableDataSource(); statDataSource = new MatTableDataSource(); pageEvent: PageEvent; jobStatusEnum = JobStatus; appliedFilters: FilterItem[]; @ViewChild(FilterComponent) filterComponent: FilterComponent; sort = ''; sortDirection = ''; constructor(private jobService: JobService, appService: AppService, snackBar: MatSnackBar) { super(appService, snackBar); } ngOnInit(): void { // By default, job page requires list in desc order by timestamp. this.sort = 'timestamp'; this.sortDirection = 'desc'; this.filterComponent.setSelectorList(Job); this.getCount(); this.getStatistics(); this.getJobs(this.pageSize, this.pageSize * this.pageIndex); } /** Gets a total count of jobs. */ getCount(observer = this.getDefaultCountObservable()) { const filterJSON = (this.appliedFilters) ? JSON.stringify(this.appliedFilters) : ''; this.jobService.getCount(filterJSON).subscribe(observer); } /** Gets jobs. * @param size A number, at most this many results will be returned. * @param offset A Number of results to skip. */ getJobs(size = 0, offset = 0) { this.loading = true; const filterJSON = (this.appliedFilters) ? JSON.stringify(this.appliedFilters) : ''; this.jobService.getJobs(size, offset, filterJSON, this.sort, this.sortDirection) .subscribe( (response) => { this.loading = false; if (this.count >= 0) { let length = 0; if (response.jobs) { length = response.jobs.length; } const total = length + offset; if (response.has_next) { if (length !== this.pageSize) { this.showSnackbar('Received unexpected number of entities.'); } else if (this.count <= total) { this.getCount(); } } else { if (this.count !== total) { if (length !== this.count) { this.getCount(); } else if (this.count > total) { const countObservable = this.getDefaultCountObservable([ () => { this.pageIndex = Math.floor(this.count / this.pageSize); this.getJobs(this.pageSize, this.pageSize * this.pageIndex); } ]); this.getCount(countObservable); } } } } this.dataSource.data = response.jobs; }, (error) => this.showSnackbar(`[${error.status}] ${error.name}`) ); } /** Hooks a page event and handles properly. */ onPageEvent(event: PageEvent) { this.pageSize = event.pageSize; this.pageIndex = event.pageIndex; 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 == null || response.jobs.length === 0) ? undefined : 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) => this.showSnackbar(`[${error.status}] ${error.name}`) ); } /** Builds statistics from given jobs list */ buildStatisticsData(title, jobs) { if (jobs == null || jobs.length === 0) { return { hours: title, created: 0, completed: 0, running: 0, bootup_err: 0, infra_err: 0, expired: 0 }; } 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, }; } /** Generates text to represent in HTML with given test type. */ getTestTypeText(status: number) { if (status === undefined || status & TestType.Unknown) { return TestType[TestType.Unknown]; } const text_list = []; [TestType.ToT, TestType.OTA, TestType.Signed, TestType.Manual].forEach(function (value) { if (status & value) { text_list.push(TestType[value]); } }); return text_list.join(', '); } /** Applies a filter and get entities with it. */ applyFilters(filters) { this.pageIndex = 0; this.appliedFilters = filters; this.getCount(); this.getJobs(this.pageSize, this.pageSize * this.pageIndex); } }