blob: 43755813dcf65fc395f32ff4d62ade15c791e166 [file] [log] [blame]
/**
* 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<Job>();
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);
}
}