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