diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 85c47d9dcae7913ae3dca475948d7e49842e4ad0..11341d757780073d68e8580f4ba75dd9d3d0a9d9 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -33,7 +33,6 @@ import {JobspecTargetComponent} from './jobspecs/jobspec/jobspec-detail/jobspec- import {JobspecInputComponent} from './jobspecs/jobspec/jobspec-detail/jobspec-input/jobspec-input.component'; import {JobspecExecutionComponent} from './jobspecs/jobspec/jobspec-detail/jobspec-execution/jobspec-execution.component'; import {ExecutionDetailComponent} from './executions/execution/execution-detail/execution-detail.component'; -import {ExecutionDetailPlanesComponent} from './executions/execution/execution-detail/execution-detail-planes/execution-detail-planes.component'; import {FontAwesomeModule} from "@fortawesome/angular-fontawesome"; import {LoadingComponent} from './loading/loading.component'; import {QueueSettingsComponent} from './settings/queue-settings/queue-settings.component'; @@ -77,7 +76,6 @@ export function init_app(configService: ConfigurationService) { JobspecInputComponent, JobspecExecutionComponent, ExecutionDetailComponent, - ExecutionDetailPlanesComponent, LoadingComponent, QueueSettingsComponent, FutureProductComponent, diff --git a/src/app/executions/execution/execution-detail/execution-detail-planes/execution-detail-planes.component.html b/src/app/executions/execution/execution-detail/execution-detail-planes/execution-detail-planes.component.html deleted file mode 100644 index db3c0c0012d26a9cb7bf1862e902311ba0ebec49..0000000000000000000000000000000000000000 --- a/src/app/executions/execution/execution-detail/execution-detail-planes/execution-detail-planes.component.html +++ /dev/null @@ -1,13 +0,0 @@ -<form [formGroup]="planesFormGroup" (ngSubmit)="acceptPlanes()" class="mb-2"> - <h4 class="pt-2 border-top"> - Planes - </h4> - - <div class="w-100" *ngFor="let plane of getPlaneKeys()"> - <label><input type="checkbox" [formControlName]="plane" value="{{plane}}" checked/>{{plane}}</label> - </div> - - <button type="submit" class="btn btn-success btn-sm"> - Accept & Archive Selected Planes - </button> -</form> diff --git a/src/app/executions/execution/execution-detail/execution-detail-planes/execution-detail-planes.component.scss b/src/app/executions/execution/execution-detail/execution-detail-planes/execution-detail-planes.component.scss deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/app/executions/execution/execution-detail/execution-detail-planes/execution-detail-planes.component.spec.ts b/src/app/executions/execution/execution-detail/execution-detail-planes/execution-detail-planes.component.spec.ts deleted file mode 100644 index f8bab0fdfdd3f5c727fc37f94c6aaf5f97de8f57..0000000000000000000000000000000000000000 --- a/src/app/executions/execution/execution-detail/execution-detail-planes/execution-detail-planes.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {async, ComponentFixture, TestBed} from '@angular/core/testing'; - -import {ExecutionDetailPlanesComponent} from './execution-detail-planes.component'; - -describe('ExecutionDetailPlanesComponent', () => { - let component: ExecutionDetailPlanesComponent; - let fixture: ComponentFixture<ExecutionDetailPlanesComponent>; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ExecutionDetailPlanesComponent] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(ExecutionDetailPlanesComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/executions/execution/execution-detail/execution-detail-planes/execution-detail-planes.component.ts b/src/app/executions/execution/execution-detail/execution-detail-planes/execution-detail-planes.component.ts deleted file mode 100644 index 44f0b1552f0765ed729a31ce9fc77f05f8c26b07..0000000000000000000000000000000000000000 --- a/src/app/executions/execution/execution-detail/execution-detail-planes/execution-detail-planes.component.ts +++ /dev/null @@ -1,94 +0,0 @@ -import {Component, EventEmitter, Input, Output, OnDestroy, OnInit} from '@angular/core'; -import {FormControl, FormGroup} from '@angular/forms'; -import {AlertService} from '../../../../services/alert.service'; -import {JobsService} from '../../../../services/jobs.service'; -import {Job} from '../../../../model/job'; -import {Observable} from 'rxjs'; - -@Component({ - selector: 'app-execution-detail-planes', - templateUrl: './execution-detail-planes.component.html', - styleUrls: ['./execution-detail-planes.component.scss'] -}) -export class ExecutionDetailPlanesComponent implements OnInit, OnDestroy { - - @Input() job: Job; - @Output() planesWritten: EventEmitter<any> = new EventEmitter(); - planes: Observable<string>; - planeKeys: string[]; - planesFormGroup: FormGroup; - - constructor( - private jobService: JobsService, - private alertService: AlertService - ) { - // Form group to contain the plane checkboxes - this.planesFormGroup = new FormGroup({}); - this.planeKeys = null; - } - - ngOnInit(): void { - - } - - ngOnDestroy(): void { - - } - - // Reads plane names from a JSON file and returns them in a list - getPlaneKeys(): string[] { - // Return the plane names if we already fetched them - if (this.planeKeys !== null) { - return this.planeKeys; - } - - this.planeKeys = []; - - // Get the plane names from the spectral window numbers in the planes.json file - this.jobService.getPlanes(this.job.job_id).subscribe(response => { - this.planeKeys = Object.keys(response.body); - - // Create a form control for each of the plane checkboxes and add them into the same group - this.planeKeys.forEach(control => this.planesFormGroup.addControl(control, new FormControl(true))); - - // Set the planes data here to signal that the controls are done being added and the HTML can finish loading - this.planes = response.body; - }, - error => { - this.alertService.error('Could not retrieve planes from planes.json. ' + error); - }); - - return this.planeKeys; - } - - // 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); - }, - () => { - // Trigger acceptQa on the parent if there were no errors during the save - this.planesWritten.emit(); - - this.alertService.success('Planes Saved'); - }); - } - } - -} diff --git a/src/app/executions/execution/execution-detail/execution-detail.component.html b/src/app/executions/execution/execution-detail/execution-detail.component.html index aeeeacd8a1088e041214e5ffdafb201f49479f4b..c7136445ab4097e34527844351025f1ddf2c00aa 100644 --- a/src/app/executions/execution/execution-detail/execution-detail.component.html +++ b/src/app/executions/execution/execution-detail/execution-detail.component.html @@ -1,12 +1,4 @@ <ng-container *ngIf="jobDetail; else loading"> - <div class="row no-gutters" *ngIf="jobDetail.queueName !== 'se_coarse_cube_imaging' && canAcceptArchive(jobDetail.status, jobDetail.archiveStatus)"> - <div class="col"> - <button type="button" class="btn btn-success btn-sm" (click)="acceptQa()">Accept & Archive</button> - </div> - <div class="col" *ngIf="jobDetail.queueName == 'quicklook'"> - <button type="button" class="btn btn-danger btn-sm" (click)="rejectQa()">Reject & Archive</button> - </div> - </div> <p><b>SDM ID:</b> {{ job.jobspec_sdm_id }} </p> @@ -43,10 +35,33 @@ </div> </form> - <app-execution-detail-planes [job]="job" - (planesWritten)="acceptQa()" - *ngIf="jobDetail.queueName === 'se_coarse_cube_imaging' && canAcceptArchive(jobDetail.status, jobDetail.archiveStatus)"> - </app-execution-detail-planes> + <div *ngIf="canSelectPlanes()"> + <h4 class="pt-2 border-top"> + Planes + </h4> + + <div class="w-100" *ngFor="let plane of planeKeys" [formGroup]="planesFormGroup"> + <label><input type="checkbox" value="{{plane}}" checked/>{{plane}}</label> + </div> + </div> + <div *ngIf="canSelectTiles()"> + <h4 class="pt-2 border-top"> + Tiles + </h4> + + <div class="w-100" *ngFor="let tileName of tileNames" [formGroup]="tilesFormGroup"> + <label><input type="checkbox" value="{{tileName}}" formControlName="{{tileName}}" checked/>{{tileName}}</label> + </div> + </div> + + <div class="row no-gutters" *ngIf="canAcceptArchive(jobDetail.status, jobDetail.archiveStatus)"> + <div class="col"> + <button type="button" class="btn btn-success btn-sm" (click)="acceptQa()">Accept & Archive</button> + </div> + <div class="col" *ngIf="jobDetail.queueName == 'quicklook'"> + <button type="button" class="btn btn-danger btn-sm" (click)="rejectQa()">Reject & Archive</button> + </div> + </div> <h4 class="pt-2 border-top"> <fa-icon [icon]="faList"></fa-icon> diff --git a/src/app/executions/execution/execution-detail/execution-detail.component.ts b/src/app/executions/execution/execution-detail/execution-detail.component.ts index c27c4c13b543148d3eba50f88dcc461ac7101458..c290f99e957025ed5a6408cb164fbcd4afb689ec 100644 --- a/src/app/executions/execution/execution-detail/execution-detail.component.ts +++ b/src/app/executions/execution/execution-detail/execution-detail.component.ts @@ -1,59 +1,97 @@ -import {Component, EventEmitter, Input, OnDestroy, OnInit, Output} from '@angular/core'; +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} from "@angular/forms"; +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 {auditTime, takeUntil} from "rxjs/operators"; +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 { +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; - noteFormGroup: FormGroup; - public faSave = faSave; public faList = faList; public faCheckCircle = faCheckCircle; public faStickyNote = faStickyNote; public faCopy = faCopy; - public weblogUrl; + public weblogUrl: string; + planeKeys: string[] = []; + tiles: Map<string, Tile> = new Map<string, Tile>(); constructor( private configService: ConfigurationService, private jobService: JobsService, private alertService: AlertService ) { - this.noteFormGroup = new FormGroup({ - notes: new FormControl(), - id: new FormControl() - }); } ngOnInit() { - this.jobService.getJobExecution(this.job.job_id).pipe(takeUntil(this.ngUnsubscribe)).subscribe((j: JobExecution) => { + // 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); + }); + } } }); + } - // autosave the form on changes - this.noteFormGroup.valueChanges.pipe(auditTime(2000),takeUntil(this.ngUnsubscribe)).subscribe(() => this.updateNotes()); + 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 { @@ -128,9 +166,12 @@ export class ExecutionDetailComponent implements OnInit, OnDestroy { } 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.performQa(this.job.job_id, 'accept', this.collectTileIds()); } rejectQa() { @@ -141,10 +182,10 @@ export class ExecutionDetailComponent implements OnInit, OnDestroy { } } - performQa(id: number, status: string) { + performQa(id: number, status: string, selectedTiles?: number[]) { //console.log(status + 'ing QA'); - let newStatus; + let newStatus: string = ""; if (status == 'accept' && (this.job.job_status === 'QA_READY' || this.job.job_status === 'QA_MANUAL')) { newStatus = 'QA_ACCEPTED'; @@ -153,7 +194,7 @@ export class ExecutionDetailComponent implements OnInit, OnDestroy { } 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).subscribe(response => { + 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 => { @@ -162,6 +203,55 @@ export class ExecutionDetailComponent implements OnInit, OnDestroy { } }; + 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(); diff --git a/src/app/services/jobs.service.ts b/src/app/services/jobs.service.ts index 235f087737c07295f2870d68ab98c6b5f374669c..f96bd93a88fe943a8ebc4173a3927964b8c94817 100644 --- a/src/app/services/jobs.service.ts +++ b/src/app/services/jobs.service.ts @@ -1,11 +1,12 @@ import {Injectable} from '@angular/core'; -import {HttpClient, HttpParams} from "@angular/common/http"; +import {HttpClient, HttpParams, HttpHeaders} from "@angular/common/http"; import {ConfigurationService} from "../env/configuration.service"; -import {Observable} from "rxjs"; -import {map, switchMap} from "rxjs/operators"; +import {Observable, of} from "rxjs"; +import {map, switchMap, concatMap} from "rxjs/operators"; import {Job, JobExecution, JobSpec} from "../model/job"; import {FiltersService} from "./filters.service"; import {CustomHttpParamEncoder} from "../custom-http-param-encoder"; +import { Tile } from '../model/tile'; @Injectable({ providedIn: 'root' @@ -149,7 +150,7 @@ export class JobsService { } - public updateNotes(id: number, notes: string): Observable<any> { + public updateNotes(id: number, notes: string): Observable<string> { return this.http.put(this.configService.config.url + this.endPoint + 'jobs/' + id + '/notes', {notes: notes}, {observe: "response"}).pipe( map(response => { return notes; @@ -164,13 +165,22 @@ export class JobsService { } // Returns a JSON encoded string of plane information - public getPlanes(id: number): Observable<any> { - return this.http.get(this.configService.config.url + this.endPoint + 'jobs/' + id + '/planes', {observe: 'response'}).pipe( + public getPlanes(id: number): Observable<string> { + return this.http.get<string>(this.configService.config.url + this.endPoint + 'jobs/' + id + '/planes', {observe: 'response'}).pipe( map(response => { - return response; + return response.body; })); } + + // Returns a JSON encoded string of plane information + public getTiles(id: number): Observable<Tile[]> { + return this.http.get<Tile[]>(this.configService.config.url + this.endPoint + 'jobs/' + id + '/calibrationTiles', {observe: 'response'}).pipe( + map(response => { + return response.body; + }) + ); + } // Writes a string of plane names public writePlanes(id: number, planes: string): Observable<any> { return this.http.put(this.configService.config.url + this.endPoint + 'jobs/' + id + '/writePlanes', {planes}, {observe: 'response'}).pipe( @@ -187,13 +197,20 @@ export class JobsService { })); } - public performQA(id: number, status: string, queue: string) { + public performQA(id: number, status: string, queue: string, selectedTiles?: number[]) { + const ingestUrl = this.configService.config.url + this.endPoint + 'jobs/' + id + '/ingest?status=' + status + '&queue=' + queue; return this.http.put(this.configService.config.url + this.endPoint + 'jobs/' + id + '/status?status=' + status + '&queue=' + queue, {}, {observe: "response"}).pipe( switchMap(response => { - return this.http.put(this.configService.config.url + this.endPoint + 'jobs/' + id + '/ingest?status=' + status + '&queue=' + queue, {}, {observe: "response"}).pipe( - map(response => { - return response.status; - })); + return this.http.put(ingestUrl, {}, {observe: "response"}).pipe( + switchMap(response => { + if (selectedTiles) { + const tileUrl = this.configService.config.url + this.endPoint + 'jobs/' + id + '/calAutoPimsTiles'; + return this.http.post<Tile[]>(tileUrl, selectedTiles, {observe: "response"}).pipe(map(response => response.body)) + } else { + return of(response.body); + } + }) + ); })); } diff --git a/src/env.js b/src/env.js index 242eedabf9f65e0d2d3b326eff34a38f39c29f68..9c3d44a652cb0f8d07f8bd3a1b3c883afdd9c584 100644 --- a/src/env.js +++ b/src/env.js @@ -18,7 +18,7 @@ A service in angular will capture this info and add it to a service window.__env.configUrl = 'https://archive-new.nrao.edu/VlassMngr/services/configuration'; break; case 'localhost': - window.__env.configUrl = 'http://localhost:8080/VlassMngr/services/configuration'; + window.__env.configUrl = 'http://localhost:4444/VlassMngr/services/configuration'; break; default: window.__env.configUrl = 'https://webtest.aoc.nrao.edu/VlassMngr/services/configuration'; diff --git a/vlass-nginx.local.conf b/vlass-nginx.local.conf new file mode 100644 index 0000000000000000000000000000000000000000..073da79a20bd4f2ea4618c2bbf3cf4251d60d7f9 --- /dev/null +++ b/vlass-nginx.local.conf @@ -0,0 +1,41 @@ +# Copyright 2024 Associated Universities, Inc. +# +# This file is part of Telescope Time Allocation Tools (TTAT). +# +# TTAT is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# any later version. +# +# TTAT is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with TTAT. If not, see <https://www.gnu.org/licenses/>. +# +http { +server { + listen 8888; + # client_max_body_size 50m; + + location / { + proxy_pass http://localhost:4200; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + } + + location /VlassMngr { + proxy_pass http://localhost:8081/VlassMngr; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + } + + # error_page 500 502 503 504 /50x.html; + # location = /50x.html { + # root /usr/share/nginx/html; + # } +} +} +events {}