Skip to content
Snippets Groups Projects
Commit c900e4ff authored by Charlotte Hausman's avatar Charlotte Hausman
Browse files

Merge pull request #5 in SSA/vlass_manager_ui from release/2.0 to main

* commit '6debf05c':
  adding the total summary numbers back for calibrations on tile progress summary
  tile summary dark mode style fix
  removed tile counts from summary as they were not intended to be there
  stopped execution tasks list from printing the results property as it caused confusion in some cases but no one seemed to know why were doing it to begin with.
  SSA-6467: Display the summary of progress on the minitiles page - part 2
  SSA-6467: Display the summary of progress on the minitiles page
  disable generate products button (products tab) while a request is in flight as to avoid double clicking on it. If a successful response is received from the request, the form is reset and closed to avoid double submission.
  disable generate products button (products tab) while a request is in flight as to avoid double clicking on it. If a successful response is received from the request, the form is reset and closed to avoid double submission.
  file edit links on jobspec page now open in a new tab
  changed default for execution warning dates span
  removing file
  updated favicon and added some title setting domain matches for the new server names
  altered job execution list status color for QA_MANUAL to match the jobspect detail views colors
  added reload buttons to product, jobs, and executions tabs
  adjusted height of notes field
  adjust formatting on paste script to skip the parent object. Also added a base-href param to the deply script so it can resolve dependencies when loaded from a deep link.
  autosave notes; save notes on archive and accept, remove formatting from paste
  resetting env.js
  Set product file links to open in a new tab. Execution notes field now scrolls so the save button is always visible when updating the field.
  added a service that tracks local job submission actions (can track others, but only implimented for jobs). Submitting jobs will lock that job from being submitted again (local only) for 5 minutes.
parents f16edc4b 6debf05c
No related branches found
Tags 2.0.0
No related merge requests found
Showing
with 158 additions and 11410 deletions
......@@ -22,7 +22,7 @@
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": false,
"aot": true,
"assets": [
"src/favicon.ico",
"src/assets",
......@@ -31,8 +31,7 @@
"styles": [
"src/styles.scss"
],
"scripts": [],
"es5BrowserSupport": true
"scripts": []
},
"configurations": {
"production": {
......
......@@ -21,7 +21,7 @@ case $DESTINATION in
;;
esac
ng build --optimization=true --prod=true
ng build --optimization=true --prod=true --base-href /vlass-manager/
ssh $USER@$SERVER "rm -R /home/${SERVER}/content/vlass-manager/*"
scp -r dist/vlass-manager/* $USER@$SERVER:/home/$SERVER/content/vlass-manager
ssh $USER@$SERVER "chmod -R 775 /home/${SERVER}/content/vlass-manager/*"
This diff is collapsed.
......@@ -57,9 +57,11 @@ export class AppComponent {
let title = 'VLASS Manager'
switch (this.window.location.hostname) {
case 'archive-test.nrao.edu':
case 'data-test.nrao.edu':
title = 'TEST | ' + title;
break;
case 'archive-new.nrao.edu':
case 'data.nrao.edu':
// leave the title alone
break;
case 'webtest.aoc.nrao.edu':
......
......@@ -37,6 +37,8 @@ import {FontAwesomeModule} from "@fortawesome/angular-fontawesome";
import {LoadingComponent} from './loading/loading.component';
import {QueueSettingsComponent} from './settings/queue-settings/queue-settings.component';
import {WINDOW_PROVIDERS} from "./env/window.provider";
import {FutureProductComponent} from './products/product/product-details/future-product/future-product.component';
import {ProgressComponent} from './tiles/progress/progress.component';
/*
We will 'provide' this function below to load and set the global configuration from the
......@@ -75,7 +77,9 @@ export function init_app(configService: ConfigurationService) {
JobspecExecutionComponent,
ExecutionDetailComponent,
LoadingComponent,
QueueSettingsComponent
QueueSettingsComponent,
FutureProductComponent,
ProgressComponent
],
imports: [
BrowserModule,
......
......@@ -33,6 +33,25 @@ export class ContentEditableDirective implements ControlValueAccessor {
}
}
@HostListener('paste', ['$event'])
onPaste($event) {
// after the paste, remove all the formatting
setTimeout(() => {
for(let el of this.elementRef.nativeElement.children) {
this.clearAttr(el);
}
}, 500);
}
clearAttr(el: any) {
this.renderer.removeAttribute(el, 'style');
this.renderer.removeAttribute(el, 'class');
this.renderer.removeAttribute(el, 'id');
for (let e of el.children) {
this.clearAttr(e);
}
}
/**
* Writes a new value to the element.
* This method will be called by the forms API to write
......
......@@ -40,7 +40,7 @@
</button>
</div>
<div class="col bg-dark text-light p-2 rounded" contenteditable="true" propValueAccessor="innerHTML"
formControlName="notes"></div>
formControlName="notes" style="max-height: 400px; overflow-y: scroll; overflow-x: hidden;"></div>
</div>
</form>
<h4 class="pt-2 border-top">
......@@ -61,10 +61,6 @@
{{ task.startDate}} <span class="badge badge-light border mr-3">Start</span>
{{ task.endDate}} <span class="badge badge-light border">End</span>
</div>
<div class="col-auto" *ngFor="let result of task.results | keyvalue">
{{ result.value }} <span class="badge badge-light border">{{ result.key }}</span>
</div>
<div class="col-auto">
<h6><span class="badge" [ngClass]="getTaskStatusBgClass(task.status)">
{{ task.status }}
......
import {Component, EventEmitter, Input, OnDestroy, OnInit, Output} from '@angular/core';
import {Job, JobExecution} from "../../../model/job";
import {Subscription} from "rxjs";
import {Subject} from "rxjs";
import {JobsService} from "../../../services/jobs.service";
import {FormControl, FormGroup} 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";
@Component({
selector: 'app-execution-detail',
......@@ -14,9 +15,9 @@ import {AlertService} from "../../../services/alert.service";
})
export class ExecutionDetailComponent implements OnInit, OnDestroy {
private ngUnsubscribe = new Subject<void>();
@Input() job: Job;
@Output() reload = new EventEmitter();
private jobDetail$: Subscription;
public jobDetail: JobExecution;
noteFormGroup: FormGroup;
......@@ -39,13 +40,16 @@ export class ExecutionDetailComponent implements OnInit, OnDestroy {
}
ngOnInit() {
this.jobDetail$ = this.jobService.getJobExecution(this.job.job_id).subscribe((j: JobExecution) => {
this.jobService.getJobExecution(this.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);
}
});
// autosave the form on changes
this.noteFormGroup.valueChanges.pipe(auditTime(2000),takeUntil(this.ngUnsubscribe)).subscribe(() => this.updateNotes());
}
copyToClipboard(text: string): void {
......@@ -102,6 +106,7 @@ export class ExecutionDetailComponent implements OnInit, OnDestroy {
}
acceptQa() {
this.updateNotes(); // make sure notes are saved before submitting
this.alertService.info('Accepting ' + this.job.job_id);
this.performQa(this.job.job_id, 'accept');
}
......@@ -137,9 +142,8 @@ export class ExecutionDetailComponent implements OnInit, OnDestroy {
ngOnDestroy(): void {
if (this.jobDetail$) {
this.jobDetail$.unsubscribe();
}
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
}
}
......@@ -55,6 +55,9 @@
</li>
</ul>
</div>
<div class="col-auto">
<button class="btn btn-sm btn-info" (click)="getPageInfoAndJob()"><fa-icon [icon]="faSyncAlt"></fa-icon> </button>
</div>
</div>
</div>
......
......@@ -13,7 +13,8 @@ import {
faFastBackward,
faFastForward,
faStepBackward,
faStepForward
faStepForward,
faSyncAlt
} from "@fortawesome/free-solid-svg-icons";
@Component({
......@@ -46,7 +47,8 @@ export class ExecutionsComponent implements OnInit, OnDestroy {
public faFastForward = faFastForward;
public faStepForward = faStepForward;
public faExclamationTriangle = faExclamationTriangle;
public alertAfterDays = 14;
public faSyncAlt = faSyncAlt;
public alertAfterDays = 7;
public formGroup: FormGroup;
......
......@@ -12,7 +12,7 @@
<div class="row m-0 py-2 mb-1 border-bottom align-items-center" *ngFor="let file of jobspec.files">
<div class="col-auto">
<a *ngIf="['WAITING','ERROR'].includes(jobspec.status)" class="btn btn-xs btn-outline-primary"
[routerLink]="['/fileeditor', 'specs', jobspec.id, file.contentType, file.name]">
[routerLink]="['/fileeditor', 'specs', jobspec.id, file.contentType, file.name]" target="_blank">
<fa-icon [icon]="faEdit"></fa-icon>
Edit
</a>
......@@ -30,5 +30,5 @@
Executions
</h5>
<app-jobspec-execution [execution]="execution" *ngFor="let execution of jobspec.executions"></app-jobspec-execution>
<p class="bg-light px-2 py-1" *ngIf="lastAction">Job recently submitted at {{lastAction.timestamp | date: 'medium'}}</p>
<button type="button" class="btn btn-success" *ngIf="canBeSubmitted()" (click)="submitJob()">Submit Job</button>
import {Component, Input, OnInit} from '@angular/core';
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {JobSpec} from "../../../model/job";
import {ConfigurationService} from "../../../env/configuration.service";
import {faBullseye, faCheckSquare, faEdit, faFileAlt} from "@fortawesome/free-solid-svg-icons";
import {JobsService} from "../../../services/jobs.service";
import {AlertService} from "../../../services/alert.service";
import {ActionsService} from "../../../services/actions.service";
import {Subject} from "rxjs";
import {takeUntil} from "rxjs/operators";
import {Action} from "../../../model/action";
@Component({
selector: 'app-jobspec-detail',
templateUrl: './jobspec-detail.component.html',
styleUrls: ['./jobspec-detail.component.scss']
})
export class JobspecDetailComponent implements OnInit {
export class JobspecDetailComponent implements OnInit, OnDestroy {
private ngUnsubscribe = new Subject();
@Input() jobspec: JobSpec;
public faBullseye = faBullseye;
......@@ -19,10 +24,24 @@ export class JobspecDetailComponent implements OnInit {
public faCheckSquare = faCheckSquare;
public faEdit = faEdit;
constructor(private configService: ConfigurationService, private jobsService: JobsService, private alertService: AlertService) {
public lastAction: Action;
constructor(
private configService: ConfigurationService,
private jobsService: JobsService,
private alertService: AlertService,
private actionsService: ActionsService
) {
}
ngOnInit() {
this.actionsService.actionsTaken$.pipe(takeUntil(this.ngUnsubscribe)).subscribe((actions: Array<Action>) => {
actions.forEach((a: Action) => {
if (a.type === 'jobSubmittedFromSpec' && a.id === this.jobspec.id) {
this.lastAction = a;
}
});
});
}
getConfigUrl(): string {
......@@ -30,18 +49,28 @@ export class JobspecDetailComponent implements OnInit {
}
canBeSubmitted(): boolean {
if (this.lastAction && this.lastAction.timeSince < (1000 * 60 * 5)) {
return false;
}
return this.jobspec.executions.length < 1 ||
(!!this.jobspec.lastJob && (this.jobspec.lastJob.status === 'ERROR' || this.jobspec.lastJob.status === 'WAITING'));
(!!this.jobspec.lastJob && ['ERROR', 'WAITING'].indexOf(this.jobspec.lastJob.status) > -1);
}
submitJob(): void {
this.alertService.info('Submitting Job...');
this.actionsService.addAction('jobSubmittedFromSpec', this.jobspec.id);
this.jobsService.submitJob(this.jobspec.id, this.jobspec.queueName).subscribe(response => {
this.alertService.success('Job Submitted');
},
error => {
this.actionsService.removeActionByTypeAndId('jobSubmittedFromSpec', this.jobspec.id);
this.alertService.error('Job Submition Failed');
});
}
ngOnDestroy() {
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
}
}
......@@ -31,6 +31,8 @@ export class JobspecExecutionComponent implements OnInit, OnDestroy {
return 'badge-warning';
case 'PROCESSING':
return 'badge-primary';
case 'QA_MANUAL':
return 'badge-warning';
default:
return 'badge-info';
}
......
......@@ -57,6 +57,9 @@
</li>
</ul>
</div>
<div class="col-auto">
<button class="btn btn-sm btn-info" (click)="getPageInfoAndJobSpec()"><fa-icon [icon]="faSyncAlt"></fa-icon> </button>
</div>
</div>
</div>
......
......@@ -10,7 +10,13 @@ import {FiltersService} from "../services/filters.service";
import {Setting} from "../model/setting";
import {SettingsService} from "../services/settings.service";
import {Epoch} from "../model/epoch";
import {faFastBackward, faFastForward, faStepBackward, faStepForward} from "@fortawesome/free-solid-svg-icons";
import {
faFastBackward,
faFastForward,
faStepBackward,
faStepForward,
faSyncAlt
} from "@fortawesome/free-solid-svg-icons";
@Component({
selector: 'app-jobspecs',
......@@ -46,6 +52,8 @@ export class JobspecsComponent implements OnInit, OnDestroy {
public faStepBackward = faStepBackward;
public faFastForward = faFastForward;
public faStepForward = faStepForward;
public faSyncAlt = faSyncAlt;
constructor(
private jobService: JobsService,
......@@ -162,7 +170,6 @@ export class JobspecsComponent implements OnInit, OnDestroy {
this.pages$ = this.jobService.getJobSpecRecordCount(epoch.id, queue.name, this.pattern, status).subscribe((jobSpecNumber: number) => {
this.numResults = jobSpecNumber;
this.pages = Math.ceil(jobSpecNumber / this.resultsPerPage);
console.log('num', jobSpecNumber);
if (this.currentPage > this.pages) {
this.currentPage = this.pages;
}
......
import {Action} from './action';
describe('Action', () => {
it('should create an instance', () => {
expect(new Action()).toBeTruthy();
});
});
export class Action {
type: string;
timestamp: Date;
id: string | number;
constructor(type: string, id: string | number) {
this.type = type;
this.id = id;
this.timestamp = new Date();
}
get timeSince(): number {
return Date.now() - this.timestamp.getTime();
}
}
......@@ -53,6 +53,8 @@ export class Product {
epoch: number;
extraInfoForJobName: string;
fileNames: Array<string>;
futureProductId: number;
futureProductName: string;
id: number;
latestProductVersion: object;
minitiles: Array<Tile>;
......
......@@ -29,3 +29,23 @@ export class TileDefinition {
coarseCenterFrequenciesMHz: Array<number>;
phaseCentersDeg: Array<PhaseCenterDeg>;
}
export class ProgressSummary {
epoch: number;
observed: number;
calibrated: number;
imaged: number;
// first half epochs have 451 tiles, second half 448
get tiles(): number {
return this.epoch.toString().split('').pop() === '1' ? 451 : 448;
}
get numObserved(): number {
return Math.round(this.tiles * (this.observed / 100))
}
get numCalibrated(): number {
return Math.round(this.tiles * (this.calibrated / 100))
}
}
<div class="row mx-1 pt-1 border-bottom">
<div class="col-auto">
<span class="badge badge-light border">Calibration</span>
</div>
<div class="col">
<a [routerLink]="['/products']" [queryParams]="{pattern: product.futureProductId, type: 'calibration'}">
{{product.futureProductName }}
</a>
<button type="button" class="btn btn-xs btn-outline-primary border-0 ml-2" (click)="showDetails = !showDetails"
*ngIf="futureProduct">
<fa-icon [icon]="faCaretDown" *ngIf="!showDetails"></fa-icon>
<fa-icon [icon]="faCaretUp" *ngIf="showDetails"></fa-icon>
</button>
<div class="bg-light shadow p-2" *ngIf="showDetails">
<app-product-details [product]="futureProduct"></app-product-details>
</div>
</div>
</div>
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