Skip to content
Snippets Groups Projects
Commit 47149d96 authored by Sam Kagan's avatar Sam Kagan
Browse files

Merge branch 'WS-1575-tile-specific-auto-pims' into 'release/2.4.3-rc1'

Added tiles checklist for Accept-and-Archiving calibration jobs

See merge request !21
parents d9ec9d7d 1f936991
No related branches found
Tags 2.4.3-rc1
2 merge requests!25Merge 2.4.3 UI to main,!21Added tiles checklist for Accept-and-Archiving calibration jobs
Showing with 206 additions and 177 deletions
......@@ -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,
......
<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 &amp; Archive Selected Planes
</button>
</form>
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();
});
});
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');
});
}
}
}
<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 &amp; Archive</button>
</div>
<div class="col" *ngIf="jobDetail.queueName == 'quicklook'">
<button type="button" class="btn btn-danger btn-sm" (click)="rejectQa()">Reject &amp; 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 &amp; Archive</button>
</div>
<div class="col" *ngIf="jobDetail.queueName == 'quicklook'">
<button type="button" class="btn btn-danger btn-sm" (click)="rejectQa()">Reject &amp; Archive</button>
</div>
</div>
<h4 class="pt-2 border-top">
<fa-icon [icon]="faList"></fa-icon>
......
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();
......
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);
}
})
);
}));
}
......
......@@ -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';
......
# 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 {}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment