Skip to content
Snippets Groups Projects
execution-detail.component.ts 9.28 KiB
Newer Older
import {Component, EventEmitter, Input, OnDestroy, OnInit, Output, SimpleChanges, OnChanges} from '@angular/core';
import {Job, JobExecution} from "../../../model/job";
import {Subject} from "rxjs";
import {JobsService} from "../../../services/jobs.service";
import {FormControl, FormGroup, FormArray} from "@angular/forms";
import {ConfigurationService} from "../../../env/configuration.service";
import {faCheckCircle, faCopy, faList, faSave, faStickyNote} from "@fortawesome/free-solid-svg-icons";
import {AlertService} from "../../../services/alert.service";
import {takeUntil, debounceTime} from "rxjs/operators";
import { Tile } from 'src/app/model/tile';

@Component({
  selector: 'app-execution-detail',
  templateUrl: './execution-detail.component.html',
  styleUrls: ['./execution-detail.component.scss']
})
export class ExecutionDetailComponent implements OnInit, OnDestroy, OnChanges {
  noteFormGroup = new FormGroup({
    notes: new FormControl(),
    id: new FormControl()
  });
  planesFormGroup = new FormGroup({});
  tilesFormGroup = new FormGroup({});
  private ngUnsubscribe = new Subject<void>();
  @Input() job: Job;
  public jobDetail: JobExecution;

  public faSave = faSave;
  public faList = faList;
  public faCheckCircle = faCheckCircle;
  public faStickyNote = faStickyNote;
  public faCopy = faCopy;

  public weblogUrl: string;
  planeKeys: string[] = [];
  tiles: Map<string, Tile> = new Map<string, Tile>();
  constructor(
    private configService: ConfigurationService,
    private jobService: JobsService,
    private alertService: AlertService
  ) {
  }

  ngOnInit() {
    // autosave the form on changes
    this.noteFormGroup.valueChanges.pipe(debounceTime(2000),takeUntil(this.ngUnsubscribe)).subscribe(() => this.updateNotes());
  // Listen for changes to @Input properties
  // Source: https://angular.io/guide/lifecycle-hooks#onchanges
  ngOnChanges(changes: SimpleChanges) {
    const job = changes.job.currentValue as Job;
    // Only need to get these when `@Input() job` changes
    this.jobService.getJobExecution(job.job_id).pipe(takeUntil(this.ngUnsubscribe)).subscribe((j: JobExecution) => {
      if (j) {
        this.jobDetail = j;
        this.noteFormGroup.get('notes').setValue(j.notes);
        this.noteFormGroup.get('id').setValue(j.id);
        // this.loadWeblogUrl, this.canSelectTiles, and this.canSelectPlanes depend on this.jobDetail,
        //  so need to wait for it to be set to run them
        if (this.canSelectTiles()) {
          this.jobService.getTiles(job.job_id).pipe(takeUntil(this.ngUnsubscribe)).subscribe(
            (response: Tile[]) => this.refreshTiles(response),
            error => {
              this.alertService.error(`Could not retrieve tiles: ${error}`);
          });
        }
        if (this.canSelectPlanes()) {
          this.jobService.getPlanes(job.job_id).subscribe((response: object) => {
            // Sort the returned planes before making form controls from them
            this.planeKeys = Object.keys(response)
              .sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
            for (const controlName in this.planesFormGroup.controls) {
              this.planesFormGroup.removeControl(controlName);
            }
            this.planeKeys.forEach(key => this.planesFormGroup.addControl(key, new FormControl(true)));
          },
            error => {
            this.alertService.error('Could not retrieve planes from planes.json. ' + error);
          });
        }
  }

  refreshTiles(newTiles: Tile[]) {
    if (this.job.jobspec_name.includes('_')) {
      // Sort newTiles by their order in the jobspec name before they're stored on this component
      const tileOrder: string[] = this.job.jobspec_name.split('_')[1].split('.');
      newTiles = newTiles.sort((a, b) => tileOrder.indexOf(a.name) - tileOrder.indexOf(b.name));
    }

    this.tiles = new Map<string, Tile>(newTiles.map(tile => [tile.name, tile]));
    for (const controlName in this.tilesFormGroup.controls) {
      this.tilesFormGroup.removeControl(controlName);
    }
    Array.from(this.tiles.keys()).forEach(tile => this.tilesFormGroup.addControl(tile, new FormControl(true)));
  }

  copyToClipboard(text: string): void {
    const selBox = document.createElement('textarea');
    selBox.style.position = 'fixed';
    selBox.style.left = '0';
    selBox.style.top = '0';
    selBox.style.opacity = '0';
    selBox.value = text;
    document.body.appendChild(selBox);
    selBox.focus();
    selBox.select();
    document.execCommand('copy');
    document.body.removeChild(selBox);
  }

  getTaskStatusBgClass(status: string) {
    switch (status) {
      case 'ERROR':
        return 'badge-danger';
      case 'SUCCESS':
        return 'badge-success';
      default:
        return 'badge-info';
    }
  }

  canAcceptArchive(status: string, archiveStatus: string): boolean {
    return (status === 'QA_READY' || status === 'QA_MANUAL' || archiveStatus === 'ARCHIVE_ERROR');
  }

  getConfigRootDataDirectory(): string {
    return this.configService.config.rootDataDirectory;
  }

  getConfigWebLogBaseUrl(): string {
    return this.configService.config.weblogbaseurl;
  }

  loadWeblogUrl(): void {
    // The base URL opens up to a file navigation URL, that's the default if the service doesn't find a pipeline directory
    let weblogUrl = this.getConfigWebLogBaseUrl() + '/vlass/weblog/' + this.jobDetail.queueName + '/' + this.job.job_name;

    this.jobService.getWeblogLink(this.job.job_id).subscribe(response => {
        // Append the pipeline directory and /html to the base weblog URL (both contained in the response)
        if (response !== null && response.body !== null && response.body !== '') {
          weblogUrl = weblogUrl + '/' + response.body;
        }
      },
      error => {
        this.alertService.error('Could not find a pipeline dir, using the base weblog URL.');
      },
      () => {
        this.weblogUrl = weblogUrl;
      });
  }

  updateNotes() {
    this.alertService.info('Saving Notes');
    const notes = this.noteFormGroup.get('notes').value;
    const id = this.noteFormGroup.get('id').value;
    this.jobService.updateNotes(id, notes).subscribe(response => {
        this.alertService.success('Notes Saved');
      },
      error => {
        this.alertService.error('Notes did not save. ' + error);
      });
  }

  acceptQa() {
    if (this.canSelectPlanes()) {
      this.acceptPlanes();
    }
    this.updateNotes(); // make sure notes are saved before submitting
    this.alertService.info('Accepting ' + this.job.job_id);
    this.performQa(this.job.job_id, 'accept', this.collectTileIds());
  }

  rejectQa() {
    let yesno = confirm("Are you sure you want to reject this image?");
    if (yesno) {
      this.alertService.info('Rejecting ' + this.job.job_id);
      this.performQa(this.job.job_id, 'reject');
    }
  }

  performQa(id: number, status: string, selectedTiles?: number[]) {
    let newStatus = '';
    const qaJobStatus = (this.job.job_status === 'QA_READY' || this.job.job_status === 'QA_MANUAL');
      newStatus = 'QA_ACCEPTED';
    } else if (status === 'reject' && qaJobStatus) {
      newStatus = 'QA_REJECTED';
    }

    if (['QA_ACCEPTED', 'QA_MANUAL_ACCEPTED', 'QA_REJECTED', 'QA_MANUAL_REJECTED'].indexOf(newStatus) > -1) {
      this.jobService.performQA(this.job.job_id, newStatus, this.jobDetail.queueName, selectedTiles).subscribe(response => {
        this.alertService.success('QA Performed for ' + id);
        this.reload.emit('reload');
      }, error => {
        this.alertService.error('QA Failed for ' + id);
  canSelectPlanes(): boolean {
    return (this.jobDetail.queueName === 'se_coarse_cube_imaging' &&
            this.canAcceptArchive(this.jobDetail.status, this.jobDetail.archiveStatus));
  canSelectTiles(): boolean {
    return this.jobDetail.queueName === 'calibration' && this.canAcceptArchive(this.jobDetail.status, this.jobDetail.archiveStatus);
  get tileNames(): string[] {
    return Array.from(this.tiles.keys());
  collectTileIds(): number[] {
    const formTiles = this.tilesFormGroup.value;
    const tileIds: number[] = [];
    for (const tileName in formTiles) {
      if (formTiles[tileName] === true) {
        tileIds.push(this.tiles.get(tileName).id);
      }
    }
    return tileIds;
  }

  // Writes to a file in lustre to flag planes to be accepted
  acceptPlanes(): void {
    this.alertService.info('Flagging accepted planes to be cached');

    // Collect the selected planes and write their names separated by a newline
    let planesText = '';
    const planes = this.planesFormGroup.value;
    for (const planeKey in planes) {
      if (planes[planeKey] === true) {
    planesText = planesText.trim();

    // Write out the planes string if any are selected
    if (planesText.length > 0) {
      this.jobService.writePlanes(this.job.job_id, planesText).subscribe(
        result => {},
        error => {
          this.alertService.error('Planes did not save. ' + error);
        },
        () => {
          this.alertService.success('Planes Saved');
        });
    }
  }

  ngOnDestroy(): void {
    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();