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; @Output() reload = new EventEmitter(); 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 this.loadWeblogUrl(); 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: string) => { this.planeKeys = response.split(/\r?\n|\r|\n/g); // Source: https://stackoverflow.com/a/21712066 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[]) { this.tiles = new Map<string, Tile>(newTiles.map(tile => {return [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 { if (status === 'QA_READY' || status === 'QA_MANUAL' || archiveStatus === 'ARCHIVE_ERROR') { return true; } else { return false; } } 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'); let notes = this.noteFormGroup.get('notes').value; let 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[]) { //console.log(status + 'ing QA'); let newStatus: string = ""; if (status == 'accept' && (this.job.job_status === 'QA_READY' || this.job.job_status === 'QA_MANUAL')) { newStatus = 'QA_ACCEPTED'; } else if (status == 'reject' && (this.job.job_status === 'QA_READY' || this.job.job_status === 'QA_MANUAL')) { 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_continuum_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; Object.keys(planes).forEach(key => { if (planes[key] === true) { planesText += key + '\n'; } }); 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(); } }