diff --git a/.gitignore b/.gitignore index 86d943a9b2e8f3bb69fbe37fd8363962646b1d92..a35c24362d324a3fbd63f04b5d4dd8da9d26e651 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /dist /tmp /out-tsc +package-lock.json # Only exists if Bazel was run /bazel-out diff --git a/deploy.sh b/deploy.sh index e68fbcede16b7ca2310f747022a9f38160358f95..36eb4ca1ca2bcc5397ec2acb7c660f46202e2155 100755 --- a/deploy.sh +++ b/deploy.sh @@ -22,7 +22,6 @@ case $DESTINATION in esac ng build --optimization=true --prod=true -ssh-add ~/.ssh/ssa16 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/*" diff --git a/src/app/app.component.html b/src/app/app.component.html index 73030732527ddafbac9d723773d994969df87204..bcc916a91c68fdf865f0ce3fb1de88e53769af44 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -22,10 +22,10 @@ <fa-icon [icon]="faSlidersH"></fa-icon> Settings </a> - <a class="col-auto" (click)="signIn()" href="javascript: void(0);" *ngIf="!isLoggedIn()"> + <!--<a class="col-auto" (click)="signIn()" href="javascript: void(0);" *ngIf="!isLoggedIn()"> <fa-icon [icon]="faSignInAlt"></fa-icon> Sign In - </a> + </a>--> <a class="col-auto" (click)="signOut()" href="javascript: void(0);" *ngIf="isLoggedIn()"> <fa-icon [icon]="faSignOutAlt"></fa-icon> Sign Out diff --git a/src/app/app.component.ts b/src/app/app.component.ts index b3b4bb95abb09b721a781d3a3df978fa7f2a1e14..10e419e529002abb5a6258b27fd4129c1fc6ede8 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -18,6 +18,8 @@ import { faToggleOn } from "@fortawesome/free-solid-svg-icons"; import {DOCUMENT} from "@angular/common"; +import {WINDOW} from "./env/window.provider"; +import {Title} from "@angular/platform-browser"; @Component({ selector: 'app-root', @@ -42,13 +44,35 @@ export class AppComponent { public darkMode = false; constructor( + private titleService: Title, private authService: AuthService, private storageService: StorageService, private router: Router, private route: ActivatedRoute, private alertService: AlertService, - @Inject(DOCUMENT) document + @Inject(DOCUMENT) document, + @Inject(WINDOW) private window: Window ) { + // set the title based on the host + let title = 'VLASS Manager' + switch (this.window.location.hostname) { + case 'archive-test.nrao.edu': + title = 'TEST | ' + title; + break; + case 'archive-new.nrao.edu': + // leave the title alone + break; + case 'webtest.aoc.nrao.edu': + title = 'DEV | ' + title; + break; + case 'localhost': + title = 'LOCAL | ' + title; + break; + default: + title = '~?~ | ' + title + break; + } + this.titleService.setTitle(title); /* this.route.queryParams.subscribe(params => { if (params.hasOwnProperty('ticket')) { @@ -85,9 +109,27 @@ export class AppComponent { } signOut(): void { + // delete all the cookies + const cookies = document.cookie.split("; "); + for (let c = 0; c < cookies.length; c++) { + const d = window.location.hostname.split("."); + while (d.length > 0) { + const cookieBase = encodeURIComponent(cookies[c].split(";")[0].split("=")[0]) + '=; expires=Thu, 01-Jan-1970 00:00:01 GMT; domain=' + d.join('.') + ' ;path='; + const p = location.pathname.split('/'); + document.cookie = cookieBase + '/'; + while (p.length > 0) { + document.cookie = cookieBase + p.join('/'); + p.pop(); + } + d.shift(); + } + } + window.location.replace('https://my.nrao.edu/cas/logout?url=' + window.location.href); + /* this.alertService.success('You are logged out.'); this.storageService.deleteLocal('jwt'); this.router.navigate(['/']); + */ } setDarkMode(dm: boolean): void { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 43114c275fde6615ddaab051dd0f04d62db984d8..6c7b2e238bc33f88d0fb706558db10b7d2ea18f5 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,4 +1,4 @@ -import {BrowserModule} from '@angular/platform-browser'; +import {BrowserModule, Title} from '@angular/platform-browser'; import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; import {APP_INITIALIZER, NgModule} from '@angular/core'; import {NgbModule} from "@ng-bootstrap/ng-bootstrap"; @@ -36,6 +36,7 @@ import {ExecutionDetailComponent} from './executions/execution/execution-detail/ 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"; /* We will 'provide' this function below to load and set the global configuration from the @@ -91,7 +92,7 @@ export function init_app(configService: ConfigurationService) { useFactory: init_app, deps: [ConfigurationService], multi: true - }], + }, Title, WINDOW_PROVIDERS], bootstrap: [AppComponent] }) export class AppModule { diff --git a/src/app/custom-http-param-encoder.spec.ts b/src/app/custom-http-param-encoder.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..ff712d2c6044906904fce32439754f9bec28fcbf --- /dev/null +++ b/src/app/custom-http-param-encoder.spec.ts @@ -0,0 +1,7 @@ +import {CustomHttpParamEncoder} from './custom-http-param-encoder'; + +describe('CustomHttpParamEncoder', () => { + it('should create an instance', () => { + expect(new CustomHttpParamEncoder()).toBeTruthy(); + }); +}); diff --git a/src/app/custom-http-param-encoder.ts b/src/app/custom-http-param-encoder.ts new file mode 100644 index 0000000000000000000000000000000000000000..acf9597fb62d52ff8acc09a3fa7d560d1cb3914a --- /dev/null +++ b/src/app/custom-http-param-encoder.ts @@ -0,0 +1,16 @@ +import {HttpParameterCodec} from '@angular/common/http'; + +export class CustomHttpParamEncoder implements HttpParameterCodec { + encodeKey(key: string): string { + return encodeURIComponent(key); + } + encodeValue(value: string): string { + return encodeURIComponent(value); + } + decodeKey(key: string): string { + return decodeURIComponent(key); + } + decodeValue(value: string): string { + return decodeURIComponent(value); + } +} diff --git a/src/app/env/window.provider.ts b/src/app/env/window.provider.ts new file mode 100644 index 0000000000000000000000000000000000000000..bb7ba0938ce09b1d015a3a74b34b9d20a0ad1193 --- /dev/null +++ b/src/app/env/window.provider.ts @@ -0,0 +1,12 @@ +import {FactoryProvider, InjectionToken} from '@angular/core'; + +export const WINDOW = new InjectionToken<Window>('window'); + +const windowProvider: FactoryProvider = { + provide: WINDOW, + useFactory: () => window +}; + +export const WINDOW_PROVIDERS = [ + windowProvider +] 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 5e3ac1c08576660214e774f498807408e8c4f4d3..86c8d782c195713a7147c92ee44804a5662666a9 100644 --- a/src/app/executions/execution/execution-detail/execution-detail.component.ts +++ b/src/app/executions/execution/execution-detail/execution-detail.component.ts @@ -39,7 +39,7 @@ export class ExecutionDetailComponent implements OnInit, OnDestroy { } ngOnInit() { - this.jobDetail$ = this.jobService.getJob(this.job.job_id).subscribe((j: JobExecution) => { + this.jobDetail$ = this.jobService.getJobExecution(this.job.job_id).subscribe((j: JobExecution) => { if (j) { this.jobDetail = j; this.noteFormGroup.get('notes').setValue(j.notes); diff --git a/src/app/executions/execution/execution.component.html b/src/app/executions/execution/execution.component.html index 79c7a0b38bcb547c1169d8dadcf4cc50024879ac..ad3e96fc782ec5be608feb53382fd280a3180bb2 100644 --- a/src/app/executions/execution/execution.component.html +++ b/src/app/executions/execution/execution.component.html @@ -29,7 +29,7 @@ <div class="col-2"> <div ngbDropdown> <button class="btn btn-xs" [ngClass]="getJobStatusClass()" type="button" ngbDropdownToggle> - {{ job.jobspec_status }} + {{ job.job_status }} </button> <ul ngbDropdownMenu> <li ngbDropdownItem *ngFor="let ps of productStatuses"> @@ -39,10 +39,9 @@ </div> </div> <div class="col-1"> - <h6><span class="badge ml-2" - [ngClass]="job.job_arch_status === 'ARCHIVED' ? 'badge-dark' : 'badge-light border'"> - {{ job.job_arch_status }} - </span></h6> + <h6> + <span class="badge ml-2 {{getArchiveStatusClass()}}">{{ job.job_arch_status }}</span> + </h6> </div> </div> diff --git a/src/app/executions/execution/execution.component.ts b/src/app/executions/execution/execution.component.ts index 25781a0d5165dd3cd13c5d00c1983363e85b0df5..c18de924947ae1e461a6e139c16f428318ead2f0 100644 --- a/src/app/executions/execution/execution.component.ts +++ b/src/app/executions/execution/execution.component.ts @@ -77,8 +77,8 @@ export class ExecutionComponent implements OnInit, OnChanges { return this.configService.config.weblogbaseurl; } - getJobStatusClass() { - switch (this.job.jobspec_status) { + getJobStatusClass(): string { + switch (this.job.job_status) { case 'ERROR': return 'btn-danger'; case 'QA_REJECTED': @@ -96,6 +96,17 @@ export class ExecutionComponent implements OnInit, OnChanges { } } + getArchiveStatusClass(): string { + switch (this.job.job_arch_status) { + case 'ARCHIVED': + return 'badge-dark'; + case 'ARCHIVE-ERROR': + return 'badge-error'; + default: + return 'badge-light border'; + } + } + showWarning(job: Job): boolean { if (job.jobspec_status === 'PROCESSING') { let start_date = new Date(job.job_starttime); diff --git a/src/app/executions/executions.component.html b/src/app/executions/executions.component.html index a119028cd37afedcd4c3d5a3bf55d49f3c72b09e..6df2b6cc3ab9975cd1341258c9cb3619ecdf6fe3 100644 --- a/src/app/executions/executions.component.html +++ b/src/app/executions/executions.component.html @@ -2,6 +2,16 @@ <div class="col-auto page-form"> <div class="form-row p-2 pb-0 align-items-center"> + <div class="col-auto pl-3"><b>Epoch</b>:</div> + <div class="btn-group col-auto" ngbDropdown> + <button class="btn btn-light btn-sm" type="button" id="epoch-select" ngbDropdownToggle> + {{ filters['EPOCH'].name }}</button> + <ul id="epoch-select-list" ngbDropdownMenu> + <li ngbDropdownItem *ngFor="let e of epochs" class="p-0"> + <button type="button" class="btn btn-link w-100 text-left" (click)="setEpoch(e)">{{e.name}}</button> + </li> + </ul> + </div> <div class="col-auto pl-3"><b>Queue</b>:</div> <div class="btn-group col-auto" ngbDropdown> <button class="btn btn-light btn-sm" type="button" ngbDropdownToggle>{{ filters['JOB_QUEUE'].label }}</button> @@ -20,15 +30,72 @@ </li> </ul> </div> - <form (ngSubmit)="getJobs()" class="form-inline col-auto pl-4" [formGroup]="formGroup"> + <form class="form-inline col-auto pl-4" [formGroup]="formGroup"> <div class="form-group"> <input type="text" class="form-control" id="pattern" placeholder="Pattern" formControlName="pattern"> </div> </form> + + <div class="col-auto pl-3"><b>Sort By</b>:</div> + <div class="btn-group col-auto" ngbDropdown> + <button class="btn btn-light btn-sm" type="button" id="sort-select" ngbDropdownToggle> + {{ getPrettyName(filters['JOB_SORT']) }}</button> + <ul ngbDropdownMenu> + <li ngbDropdownItem *ngFor="let col of sortColumns"> + <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setSortColumn(col)">{{getPrettyName(col)}}</button> + </li> + </ul> + </div> + <div class="btn-group col-auto" ngbDropdown> + <button class="btn btn-light btn-sm" type="button" id="sort-direction" ngbDropdownToggle> + {{ filters['SORT_DIRECTION'] }}</button> + <ul ngbDropdownMenu> + <li ngbDropdownItem *ngFor="let direction of sortDirections"> + <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setSortDirection(direction)">{{direction}}</button> + </li> + </ul> + </div> + </div> + </div> + + <div class="col text-nowrap"> + <p class="text-danger text-right p-2 m-0">{{ ((currentPage - 1) * resultsPerPage) + 1 }} + - {{ currentPage * resultsPerPage < numResults ? currentPage * resultsPerPage : numResults}} + of {{ numResults }}</p> + </div> + <div class="col-auto"> + + <div class="form-group m-0"> + <div class="btn-group"> + <button id="to-first-page" class="btn btn-danger btn-sm" (click)="goToPage(1)"> + <fa-icon [icon]="faFastBackward"></fa-icon> + </button> + <button id="back-one-page" class="btn btn-danger btn-sm" (click)="goToPage(currentPage-1)"> + <fa-icon [icon]="faStepBackward"></fa-icon> + </button> + <div class="btn-group" ngbDropdown> + <button class="btn btn-danger btn-sm" type="button" id="page-select" ngbDropdownToggle> + Page {{currentPage}}</button> + <ul style="height: 200px; overflow-y: scroll;" ngbDropdownMenu> + <li ngbDropdownItem *ngFor="let p of getPages(); let i = index" class="p-0"> + <button type="button" class="btn btn-link w-100 text-left" (click)="goToPage(i+1)">Page {{i + 1}}</button> + </li> + </ul> + </div> + <button id="forward-one-page" class="btn btn-danger btn-sm" (click)="goToPage(currentPage+1)"> + <fa-icon [icon]="faStepForward"></fa-icon> + </button> + <button id="to-last-page" class="btn btn-danger btn-sm" (click)="goToPage(pages)"> + <fa-icon [icon]="faFastForward"></fa-icon> + </button> + </div> </div> </div> +</div> + +<div class="row pb-2"> <div class="col"></div> - <form class="col-auto form-inline" [formGroup]="alertThresholdForm"> + <form class="col-auto form-inline text-right" [formGroup]="alertThresholdForm"> <span class="text-warning ml-4 mr-2"><fa-icon [icon]="faExclamationTriangle"></fa-icon></span> after <input type="number" min="1" class="form-control form-control-sm mx-2 w-25" formControlName="threshold"/> days </form> @@ -44,7 +111,7 @@ </div> <ng-container *ngFor="let job of jobs"> <app-execution-job [job]="job" [queue]="filters['JOB_QUEUE']" [alertThresholdDays]="alertAfterDays" - (reload)="getJobs()"></app-execution-job> + (reload)="getJobs(currentPage)"></app-execution-job> </ng-container> </ng-container> diff --git a/src/app/executions/executions.component.ts b/src/app/executions/executions.component.ts index bd685fe1e0a5a7ed571664809b6878b0a10d7e5c..8bd7723d0b2edc60fdb4f1347984f3f1e29ea5a0 100644 --- a/src/app/executions/executions.component.ts +++ b/src/app/executions/executions.component.ts @@ -7,7 +7,14 @@ import {debounceTime, distinctUntilChanged, map} from "rxjs/operators"; import {ActivatedRoute} from "@angular/router"; import {AlertService} from "../services/alert.service"; import {FiltersService} from "../services/filters.service"; -import {faExclamationTriangle} from "@fortawesome/free-solid-svg-icons"; +import {Epoch} from "../model/epoch"; +import { + faExclamationTriangle, + faFastBackward, + faFastForward, + faStepBackward, + faStepForward +} from "@fortawesome/free-solid-svg-icons"; @Component({ selector: 'app-executions', @@ -16,15 +23,28 @@ import {faExclamationTriangle} from "@fortawesome/free-solid-svg-icons"; }) export class ExecutionsComponent implements OnInit, OnDestroy { + public epochs: Array<Epoch>; public queues: Array<JobQueue>; public statuses: Array<string>; public jobs$: Subscription; public jobs: Array<Job>; public pattern: string = ""; + public resultsPerPage: number = 100; + public currentPage: number = 1; + public numResults: number = 0; + public pages: number = 1; + private pages$: Subscription; + private filters$: Subscription; public filters: object; + public sortColumns: Array<string>; + public sortDirections: Array<string>; + public faFastBackward = faFastBackward; + public faStepBackward = faStepBackward; + public faFastForward = faFastForward; + public faStepForward = faStepForward; public faExclamationTriangle = faExclamationTriangle; public alertAfterDays = 14; @@ -38,8 +58,11 @@ export class ExecutionsComponent implements OnInit, OnDestroy { private alertService: AlertService, private filterService: FiltersService ) { + this.epochs = this.filterService.getFilter('EPOCH'); this.queues = this.filterService.getFilter('JOB_QUEUE'); this.statuses = this.filterService.getFilter('JOB_STATUS'); + this.sortColumns = this.filterService.getFilter('JOB_SORT'); + this.sortDirections = this.filterService.getFilter('SORT_DIRECTION'); } ngOnInit() { @@ -55,6 +78,18 @@ export class ExecutionsComponent implements OnInit, OnDestroy { if (params.hasOwnProperty('status')) { this.filterService.setCurrentSetting('JOB_STATUS', params.status); } + if (params.hasOwnProperty('queue')) { + const paramQueue = Job.getQueueFromName(params.queue); + if (paramQueue) { + this.filterService.setCurrentSetting('JOB_QUEUE', paramQueue); + } + } + if (params.hasOwnProperty('epoch')) { + const paramEpoch = Epoch.getEpochFromId(params.epoch); + if (paramEpoch) { + this.filterService.setCurrentSetting('EPOCHS', paramEpoch); + } + } }); this.formGroup.get('pattern').valueChanges.pipe( @@ -63,12 +98,12 @@ export class ExecutionsComponent implements OnInit, OnDestroy { map(results => results) ).subscribe((val: string) => { this.pattern = val; - this.getJobs(); + this.getPageInfoAndJob(); }); this.filters$ = this.filterService.currentSettings$.subscribe((filters: object) => { this.filters = filters; - this.getJobs(); + this.getPageInfoAndJob(); }); this.alertThresholdForm = new FormGroup({ @@ -84,38 +119,109 @@ export class ExecutionsComponent implements OnInit, OnDestroy { }); } - setQueue(queue: JobQueue) { + setEpoch(epoch: Epoch) { + this.filterService.setCurrentSetting('EPOCH', epoch); + } + + setQueue(queue: JobQueue): void { this.filterService.setCurrentSetting('JOB_QUEUE', queue); } - setStatus(status: string) { + setStatus(status: string): void { this.filterService.setCurrentSetting('JOB_STATUS', status); } - getJobs() { + setSortColumn(column: string): void { + this.filterService.setCurrentSetting('JOB_SORT', column); + } + + setSortDirection(direction: string): void { + this.filterService.setCurrentSetting('SORT_DIRECTION', direction); + } + + getPrettyName(name: string): string { + return FiltersService.prettyName(name); + } + + goToPage(page: number): boolean { + if (page < 1) { + page = 1; + } + if (page > this.pages) { + page = this.pages; + } + if (this.currentPage == page) { + return false; + } + this.currentPage = page; + this.getJobs(page); + } + + getPages(): Array<any> { + return new Array(this.pages); + } + + getPageCount(): void { + const epoch = this.filterService.getCurrentSetting('EPOCH'); + const queue = this.filterService.getCurrentSetting('JOB_QUEUE'); + const status = this.filterService.getCurrentSetting('JOB_STATUS'); + this.pages$ = this.jobService.getJobRecordCount(epoch.id, queue.name, this.pattern, status).subscribe((jobNumber: number) => { + this.numResults = jobNumber; + this.pages = Math.ceil(jobNumber / this.resultsPerPage); + if (this.currentPage > this.pages) { + this.currentPage = this.pages; + } + if (this.currentPage < 1) { + this.currentPage = 1; + } + this.getJobs(1); + }); + } + + getJobs(pageId: number): void { + // clear a previous call if (this.jobs$) { this.jobs$.unsubscribe(); } this.jobs = null; - var possibleId = parseInt(this.pattern); - var id = ""; - if (!isNaN(possibleId)) { - id = this.pattern; - } + const epoch = this.filterService.getCurrentSetting('EPOCH'); const queue = this.filterService.getCurrentSetting('JOB_QUEUE'); const status = this.filterService.getCurrentSetting('JOB_STATUS'); - this.alertService.info('Getting ' + queue.label + ' Jobs'); - this.jobs$ = this.jobService.getJobs(queue.name, id, this.pattern, status).subscribe((j: Array<Job>) => { - if (j && j.length > 0) { - this.alertService.success(queue.label + ' jobs retrieved'); - this.jobs = j; + this.alertService.info('Getting ' + queue.label); + this.jobs$ = this.jobService.getJobPage(epoch.id, queue.name, pageId - 1, this.pattern, status).subscribe((jobs: Array<Job>) => { + if (jobs && jobs.length > 0) { + this.alertService.success(queue.label + ' loaded.'); + this.jobs = jobs; } else { this.jobs = []; - this.alertService.error('No ' + queue.label + ' jobs found'); + this.alertService.error('No ' + queue.label + ' found.') } }); } + getJobById(id: number): void { + // clear a previous call + if (this.jobs$) { + this.jobs$.unsubscribe(); + } + this.jobs$ = this.jobService.getJobById(id).subscribe((job: Job) => { + this.jobs = [job]; + this.numResults = 1; + this.pages = 1; + this.currentPage = 1; + }); + } + + getPageInfoAndJob(): void { + let possible_id = parseInt(this.pattern); + if (isNaN(possible_id)) { + this.getPageCount(); + this.getJobs(1); + } else { + this.getJobById(possible_id); + } + } + ngOnDestroy(): void { if (this.jobs$) { this.jobs$.unsubscribe(); @@ -123,5 +229,8 @@ export class ExecutionsComponent implements OnInit, OnDestroy { if (this.filters$) { this.filters$.unsubscribe(); } + if (this.pages$) { + this.pages$.unsubscribe(); + } } } diff --git a/src/app/fileeditor/fileeditor.component.ts b/src/app/fileeditor/fileeditor.component.ts index 989df533cb5c2d26bfe019a19d46d2eb24ca1c2c..61592b5dbad1cdfafe6166fce693d4e18f36ec40 100644 --- a/src/app/fileeditor/fileeditor.component.ts +++ b/src/app/fileeditor/fileeditor.component.ts @@ -70,7 +70,6 @@ export class FileeditorComponent implements OnInit { observe: "response", responseType: "text" }).pipe(map(response => { - console.log('get file: ', response); this.formGroup.get('content').setValue(response.body); return response.body; })); @@ -96,9 +95,10 @@ export class FileeditorComponent implements OnInit { this.http.put(this.configService.config.url + '/services/' + this.displayedUrl, this.formGroup.get('content').value, { observe: "response", responseType: "text", headers: headers }).subscribe(response => { - console.log('put file: ', response); this.feedback = 'Save Successful'; return response.body; + }, error => { + this.feedback = 'Something went wrong - ' + error; }); } else { diff --git a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-detail.component.html b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-detail.component.html index 958d703e6fc8b2e6db55dec03f6619615062f967..786705add43a776aab4baaed4d846f382e5d3682 100644 --- a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-detail.component.html +++ b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-detail.component.html @@ -10,12 +10,6 @@ Files </h5> <div class="row m-0 py-2 mb-1 border-bottom align-items-center" *ngFor="let file of jobspec.files"> - <div class="col"> - <a target="_blank" - href="{{getConfigUrl()}}/services/job/specs/{{ jobspec.id }}/files/{{ file.contentType }}/{{ file.name}}"> - {{file.name}} - </a> - </div> <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]"> @@ -23,6 +17,12 @@ Edit </a> </div> + <div class="col"> + <a target="_blank" + href="{{getConfigUrl()}}/services/job/specs/{{ jobspec.id }}/files/{{ file.contentType }}/{{ file.name}}"> + {{file.name}} + </a> + </div> </div> <h5 class="mt-2"> diff --git a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-execution/jobspec-execution.component.html b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-execution/jobspec-execution.component.html index 69dd094aaa1e9d1f00c33103acc1368aed14996f..8a8e8f5f407ae8fb4daa5dea772692cf625fefe1 100644 --- a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-execution/jobspec-execution.component.html +++ b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-execution/jobspec-execution.component.html @@ -1,6 +1,6 @@ <div class="row m-0 py-2 mb-1 border-bottom align-items-center"> <div class="col"> - <a [routerLink]="['/executions']" [queryParams]="{pattern: execution.id, status: execution.status}">{{execution.name}}</a> + <a [routerLink]="['/executions']" [queryParams]="{pattern: execution.id, status: execution.status, queue: execution.queueName}">{{execution.name}}</a> </div> <div class="col-auto"> <h6><span class="badge" [ngClass]="getJobStatusClass(execution.status)">{{execution.status}}</span></h6> diff --git a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-execution/jobspec-execution.component.ts b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-execution/jobspec-execution.component.ts index dca06b00893102a88f683bd3ea77aaccf2b7fd47..d5fa1431d3cfc1e60afa692126ca2ae750e535bb 100644 --- a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-execution/jobspec-execution.component.ts +++ b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-execution/jobspec-execution.component.ts @@ -38,7 +38,7 @@ export class JobspecExecutionComponent implements OnInit, OnDestroy { getJob(jobId: number): void { this.showDetails = true; - this.job$ = this.jobService.getJob(jobId).subscribe((j: JobExecution) => { + this.job$ = this.jobService.getJobExecution(jobId).subscribe((j: JobExecution) => { if (j) { this.job = j; } else { diff --git a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-input/jobspec-input.component.html b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-input/jobspec-input.component.html index 298cbd04811431153716210d876122c17044940a..b84abdadb7815edac99c52bb7618bdb66e007d5d 100644 --- a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-input/jobspec-input.component.html +++ b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-input/jobspec-input.component.html @@ -1,5 +1,5 @@ <a [routerLink]="['/products']" - [queryParams]="{epoch: input.productEpoch, type: input.productType, pattern: input.productName}"> + [queryParams]="{epoch: input.productEpoch, type: input.productType, pattern: input.productId}"> {{input.productName}} </a> <button type="button" class="btn btn-xs btn-outline-primary border-0 ml-2" (click)="toggleDetails(input.productId)"> diff --git a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-target/jobspec-target.component.html b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-target/jobspec-target.component.html index 87ef25192351a41b4078e51215165c0960d232f6..192963b75223c963d6a392dd9a9d01a5bdfa487c 100644 --- a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-target/jobspec-target.component.html +++ b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-target/jobspec-target.component.html @@ -2,7 +2,7 @@ <div class="col"> <span class="badge badge-light border mr-1">Creating</span> <a [routerLink]="['/products']" - [queryParams]="{epoch: target.productEpoch, type: target.productType, pattern: target.id}"> + [queryParams]="{epoch: target.productEpoch, type: getProductNameTypeFromId(target.productType), pattern: target.productId}"> {{target.productName}} </a> <button type="button" class="btn btn-xs btn-outline-primary border-0 ml-2" diff --git a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-target/jobspec-target.component.ts b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-target/jobspec-target.component.ts index 08e46d3721de372ce71cbe598b7b07d1ad71cddd..5b5c430989d0bfac6af9a0468903548c3c761e3b 100644 --- a/src/app/jobspecs/jobspec/jobspec-detail/jobspec-target/jobspec-target.component.ts +++ b/src/app/jobspecs/jobspec/jobspec-detail/jobspec-target/jobspec-target.component.ts @@ -47,6 +47,10 @@ export class JobspecTargetComponent implements OnInit, OnDestroy { }); } + getProductNameTypeFromId(id: number): string { + return Product.getTypeNameFromId(id); + } + ngOnDestroy(): void { if (this.product$) { this.product$.unsubscribe(); diff --git a/src/app/jobspecs/jobspec/jobspec.component.html b/src/app/jobspecs/jobspec/jobspec.component.html index 94904acf455aa12886a9b004c0b60687d5f8c5c5..a7b8e0e19b38bf80cc24ad12d3007df3df4acc64 100644 --- a/src/app/jobspecs/jobspec/jobspec.component.html +++ b/src/app/jobspecs/jobspec/jobspec.component.html @@ -12,7 +12,7 @@ <button class="btn btn-link p-0" (click)="toggleDetails()">{{ job.jobspec_name }}</button> </ng-container> <ng-template #notHomeTabName> - <a [routerLink]="['/jobs']" [queryParams]="{pattern: job.jobspec_name}">{{job.jobspec_name}}</a> + <a [routerLink]="['/jobs']" [queryParams]="{pattern: job.jobspec_id, queuq: jobspec.queueName}">{{job.jobspec_name}}</a> <button class="btn btn-xs btn-outline-primary border-0 ml-2" (click)="toggleDetails()"> <fa-icon [icon]="faCaretDown" *ngIf="!detailsExposed"></fa-icon> <fa-icon [icon]="faCaretUp" *ngIf="detailsExposed"></fa-icon> diff --git a/src/app/jobspecs/jobspecs.component.html b/src/app/jobspecs/jobspecs.component.html index 5084396e885d9c55e35f7005c7a1eec2be44ff79..a20316cd9ddac7e6202d426f1ea14476472b7dc9 100644 --- a/src/app/jobspecs/jobspecs.component.html +++ b/src/app/jobspecs/jobspecs.component.html @@ -2,9 +2,19 @@ <div class="col-auto page-form"> <div class="form-row p-2 pb-0 align-items-center"> + <div class="col-auto pl-3"><b>Epoch</b>:</div> + <div class="btn-group col-auto" ngbDropdown> + <button class="btn btn-light btn-sm" type="button" id="epoch-select" ngbDropdownToggle> + {{ filters['EPOCH'].name }}</button> + <ul id="epoch-select-list" ngbDropdownMenu> + <li ngbDropdownItem *ngFor="let e of epochs" class="p-0"> + <button type="button" class="btn btn-link w-100 text-left" (click)="setEpoch(e)">{{e.name}}</button> + </li> + </ul> + </div> <div class="col-auto pl-3"><b>Queue</b>:</div> <div class="btn-group col-auto" ngbDropdown> - <button class="btn btn-light btn-sm" type="button" id="page-select" ngbDropdownToggle> + <button class="btn btn-light btn-sm" type="button" ngbDropdownToggle> {{ filters['JOB_QUEUE'].label }}</button> <ul ngbDropdownMenu> <li ngbDropdownItem *ngFor="let q of queues" class="p-0"> @@ -22,14 +32,67 @@ </li> </ul> </div> - <form (ngSubmit)="getJobs()" class="form-inline col-auto pl-4" [formGroup]="formGroup"> + <form class="form-inline col-auto pl-4" [formGroup]="formGroup"> <div class="form-group"> <input type="text" class="form-control" id="pattern" placeholder="Pattern" formControlName="pattern"> </div> </form> + + <div class="col-auto pl-3"><b>Sort By</b>:</div> + <div class="btn-group col-auto" ngbDropdown> + <button class="btn btn-light btn-sm" type="button" id="sort-select" ngbDropdownToggle> + {{ getPrettyName(filters['JOBSPEC_SORT']) }}</button> + <ul ngbDropdownMenu> + <li ngbDropdownItem *ngFor="let col of sortColumns"> + <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setSortColumn(col)">{{getPrettyName(col)}}</button> + </li> + </ul> + </div> + <div class="btn-group col-auto" ngbDropdown> + <button class="btn btn-light btn-sm" type="button" id="sort-direction" ngbDropdownToggle> + {{ filters['SORT_DIRECTION'] }}</button> + <ul ngbDropdownMenu> + <li ngbDropdownItem *ngFor="let direction of sortDirections"> + <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setSortDirection(direction)">{{direction}}</button> + </li> + </ul> + </div> </div> </div> + <div class="col"> + <p class="text-danger text-right p-2 m-0">{{ ((currentPage - 1) * resultsPerPage) + 1 }} + - {{ currentPage * resultsPerPage < numResults ? currentPage * resultsPerPage : numResults}} + of {{ numResults }}</p> + </div> + <div class="col-auto"> + + <div class="form-group m-0"> + <div class="btn-group"> + <button id="to-first-page" class="btn btn-danger btn-sm" (click)="goToPage(1)"> + <fa-icon [icon]="faFastBackward"></fa-icon> + </button> + <button id="back-one-page" class="btn btn-danger btn-sm" (click)="goToPage(currentPage-1)"> + <fa-icon [icon]="faStepBackward"></fa-icon> + </button> + <div class="btn-group" ngbDropdown> + <button class="btn btn-danger btn-sm" type="button" id="page-select" ngbDropdownToggle> + Page {{currentPage}}</button> + <ul style="height: 200px; overflow-y: scroll;" ngbDropdownMenu> + <li ngbDropdownItem *ngFor="let p of getPages(); let i = index" class="p-0"> + <button type="button" class="btn btn-link w-100 text-left" (click)="goToPage(i+1)">Page {{i + 1}}</button> + </li> + </ul> + </div> + <button id="forward-one-page" class="btn btn-danger btn-sm" (click)="goToPage(currentPage+1)"> + <fa-icon [icon]="faStepForward"></fa-icon> + </button> + <button id="to-last-page" class="btn btn-danger btn-sm" (click)="goToPage(pages)"> + <fa-icon [icon]="faFastForward"></fa-icon> + </button> + </div> + </div> + </div> </div> @@ -45,7 +108,7 @@ <div class="col-auto px-2 py-3 thead_th" *ngIf="canDeleteJobs"> </div> </div> <ng-container *ngFor="let job of jobs"> - <app-jobspec [job]="job" [canDeleteJob]="canDeleteJobs" (refreshJobs)="getJobs()"></app-jobspec> + <app-jobspec [job]="job" [canDeleteJob]="canDeleteJobs" (refreshJobs)="getPageInfoAndJobSpec()"></app-jobspec> </ng-container> </ng-container> diff --git a/src/app/jobspecs/jobspecs.component.ts b/src/app/jobspecs/jobspecs.component.ts index 78200007de08169c60e1765b175aa63e5b1b053d..a35424014148587114daed40a4736c3377ddd768 100644 --- a/src/app/jobspecs/jobspecs.component.ts +++ b/src/app/jobspecs/jobspecs.component.ts @@ -9,6 +9,8 @@ import {AlertService} from "../services/alert.service"; 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"; @Component({ selector: 'app-jobspecs', @@ -17,20 +19,34 @@ import {SettingsService} from "../services/settings.service"; }) export class JobspecsComponent implements OnInit, OnDestroy { + public epochs: Array<Epoch>; public queues: Array<JobQueue>; public statuses: Array<string>; public jobs$: Subscription; - public jobs: Array<Job>; + public jobs: Array<JobSpec>; public pattern: string = ""; + public resultsPerPage: number = 100; + public currentPage: number = 1; + public numResults: number = 0; + public pages: number = 1; + private pages$: Subscription; + private readonly settings$: Subscription; public canDeleteJobs = false; private filters$: Subscription; public filters: object; + public sortColumns: Array<string>; + public sortDirections: Array<string>; public formGroup: FormGroup; + public faFastBackward = faFastBackward; + public faStepBackward = faStepBackward; + public faFastForward = faFastForward; + public faStepForward = faStepForward; + constructor( private jobService: JobsService, private route: ActivatedRoute, @@ -38,6 +54,7 @@ export class JobspecsComponent implements OnInit, OnDestroy { private filterService: FiltersService, private settingsService: SettingsService ) { + this.epochs = this.filterService.getFilter('EPOCH'); this.queues = this.filterService.getFilter('JOB_QUEUE'); this.statuses = this.filterService.getFilter('JOB_STATUS'); this.settings$ = this.settingsService.getSettings().subscribe((s: Setting) => { @@ -49,6 +66,8 @@ export class JobspecsComponent implements OnInit, OnDestroy { }, error => { this.canDeleteJobs = false; }); + this.sortColumns = this.filterService.getFilter('JOBSPEC_SORT'); + this.sortDirections = this.filterService.getFilter('SORT_DIRECTION'); } ngOnInit() { @@ -70,6 +89,12 @@ export class JobspecsComponent implements OnInit, OnDestroy { this.filterService.setCurrentSetting('JOB_QUEUE', paramQueue); } } + if (params.hasOwnProperty('epoch')) { + const paramEpoch = Epoch.getEpochFromId(params.epoch); + if (paramEpoch) { + this.filterService.setCurrentSetting('EPOCHS', paramEpoch); + } + } }); this.formGroup.get('pattern').valueChanges.pipe( @@ -78,13 +103,18 @@ export class JobspecsComponent implements OnInit, OnDestroy { map(results => results) ).subscribe((val: string) => { this.pattern = val; - this.getJobs(); + this.getPageInfoAndJobSpec(); }); this.filters$ = this.filterService.currentSettings$.subscribe((filters: object) => { this.filters = filters; - this.getJobs(); + this.getPageInfoAndJobSpec(); }); + this.getPageInfoAndJobSpec(); + } + + setEpoch(epoch: Epoch) { + this.filterService.setCurrentSetting('EPOCH', epoch); } setQueue(queue: JobQueue): void { @@ -95,6 +125,99 @@ export class JobspecsComponent implements OnInit, OnDestroy { this.filterService.setCurrentSetting('JOB_STATUS', status); } + setSortColumn(column: string): void { + this.filterService.setCurrentSetting('JOBSPEC_SORT', column); + } + + setSortDirection(direction: string): void { + this.filterService.setCurrentSetting('SORT_DIRECTION', direction); + } + + getPrettyName(name: string): string { + return FiltersService.prettyName(name); + } + + goToPage(page: number): boolean { + if (page < 1) { + page = 1; + } + if (page > this.pages) { + page = this.pages; + } + if (this.currentPage == page) { + return false; + } + this.currentPage = page; + this.getJobSpecs(); + } + + getPages(): Array<any> { + return new Array(this.pages); + } + + getPageCount(): void { + const epoch = this.filterService.getCurrentSetting('EPOCH'); + const queue = this.filterService.getCurrentSetting('JOB_QUEUE'); + const status = this.filterService.getCurrentSetting('JOB_STATUS'); + 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; + } + if (this.currentPage < 1) { + this.currentPage = 1; + } + this.getJobSpecs(); + }); + } + + getJobSpecs(): void { + // clear a previous call + if (this.jobs$) { + this.jobs$.unsubscribe(); + } + this.jobs = null; + const epoch = this.filterService.getCurrentSetting('EPOCH'); + const queue = this.filterService.getCurrentSetting('JOB_QUEUE'); + const status = this.filterService.getCurrentSetting('JOB_STATUS'); + this.alertService.info('Getting ' + queue.label); + this.jobs$ = this.jobService.getJobSpecPage(epoch.id, queue.name, this.currentPage - 1, this.pattern, status).subscribe((jobSpecs: Array<JobSpec>) => { + if (jobSpecs && jobSpecs.length > 0) { + this.alertService.success(queue.label + ' loaded.'); + this.jobs = jobSpecs; + } else { + this.jobs = []; + this.alertService.error('No ' + queue.label + ' found.') + } + }); + } + + getJobSpecById(id: number): void { + // clear a previous call + if (this.jobs$) { + this.jobs$.unsubscribe(); + } + this.jobs$ = this.jobService.getJobSpecById(id).subscribe((jobSpec: JobSpec) => { + this.jobs = [jobSpec]; + this.numResults = 1; + this.pages = 1; + this.currentPage = 1; + }); + } + + getPageInfoAndJobSpec(): void { + let possible_id = parseInt(this.pattern); + if (isNaN(possible_id)) { + this.getPageCount(); + this.getJobSpecs(); + } else { + this.getJobSpecById(possible_id); + } + } + + /* getJobs(): void { if (this.jobs$) { this.jobs$.unsubscribe(); @@ -127,6 +250,7 @@ export class JobspecsComponent implements OnInit, OnDestroy { }); } } + */ ngOnDestroy(): void { if (this.jobs$) { @@ -138,6 +262,9 @@ export class JobspecsComponent implements OnInit, OnDestroy { if (this.settings$) { this.settings$.unsubscribe(); } + if (this.pages$) { + this.pages$.unsubscribe(); + } } } diff --git a/src/app/model/job.ts b/src/app/model/job.ts index 2a49d13e46b7d96829d725a01a0760093a603146..e869e996cf6a655e3bd4c0e1ab5a95208f8d2fb2 100644 --- a/src/app/model/job.ts +++ b/src/app/model/job.ts @@ -41,6 +41,8 @@ export class Job { } } } + + static SORT_COLUMNS = ['id', 'name', 'start_date', 'end_date', 'status']; } export class Task { @@ -94,4 +96,6 @@ export class JobSpec { sdmId: string; status: string; targets: Array<ProductVersion>; + + static SORT_COLUMNS = ['id', 'name', 'creation_date', 'status']; } diff --git a/src/app/model/product.ts b/src/app/model/product.ts index 465a5880830da3d3cf35851df11dccc1c8fdb163..7c33ab3ade83dbcadbc9272b4b69868755552043 100644 --- a/src/app/model/product.ts +++ b/src/app/model/product.ts @@ -93,6 +93,7 @@ export class Product { ]; static getTypeFromName(typeName: string): ProductType { + typeName = typeName === 'rawdata' ? 'schedblock' : typeName; for (const type of this.TYPES) { if (type.name === typeName) { return type; @@ -100,6 +101,14 @@ export class Product { } } + static getTypeNameFromId(typeId: number): string { + for (const type of this.TYPES) { + if (type.id === typeId) { + return type.name; + } + } + } + static getIdFromTypeName(typeName: string): number { for (const type of this.TYPES) { if (type.name === typeName) { @@ -108,4 +117,6 @@ export class Product { } } + static SORT_COLUMNS = ['id', 'name', 'status']; + } diff --git a/src/app/model/tile.ts b/src/app/model/tile.ts index 91b11aa84271ed8ed3e8664dad377e6076e2195a..38401199cf68f7da12ad14a14d49d57ee367b03e 100644 --- a/src/app/model/tile.ts +++ b/src/app/model/tile.ts @@ -16,6 +16,8 @@ export class Tile { raMin: number; tier: number; scans: Array<object>; + + static SORT_COLUMNS = ['id', 'name', 'status']; } export class PhaseCenterDeg { diff --git a/src/app/products/product/product-details/product-details.component.html b/src/app/products/product/product-details/product-details.component.html index dee3fc266cc01837dc696aa7229b6b8df76cc308..98c88dfa811fac3eea06139b96951a1fbc5a22b8 100644 --- a/src/app/products/product/product-details/product-details.component.html +++ b/src/app/products/product/product-details/product-details.component.html @@ -49,26 +49,41 @@ </div> <div *ngIf="typeConfig | async"> - <a class="btn btn-sm btn-outline-info bg-light float-right" [routerLink]="['/fileeditor','type', type.id, 'json','configurations']"> - <fa-icon [icon]="faEdit"></fa-icon> - </a> + <h5 class="my-3"> <fa-icon [icon]="faFileCode"></fa-icon> Type Parameters </h5> - <div class="bg-secondary text-light p-2 my-2">{{ (typeConfig | async) | json}}</div> + <div class="form-row my-2"> + <div class="col-auto"> + <a class="btn btn-sm btn-outline-info bg-light" + [routerLink]="['/fileeditor','type', type.id, 'json','configurations']"> + <fa-icon [icon]="faEdit"></fa-icon> + </a> + </div> + <div class="col"> + <div class="bg-secondary text-light p-2">{{ (typeConfig | async) | json}}</div> + </div> + </div> + </div> <div *ngIf="productConfig | async"> - <a class="btn btn-sm btn-outline-info bg-light float-right" - [routerLink]="['/fileeditor','product', product.id, 'json','configurations']"> - <fa-icon [icon]="faEdit"></fa-icon> - </a> <h5 class="my-3"> <fa-icon [icon]="faFileCode"></fa-icon> Product Parameters </h5> - <div class="bg-secondary text-light p-2 my-2">{{ (productConfig | async) | json}}</div> + <div class="form-row my-2"> + <div class="col-auto"> + <a class="btn btn-sm btn-outline-info bg-light" + [routerLink]="['/fileeditor','product', product.id, 'json','configurations']"> + <fa-icon [icon]="faEdit"></fa-icon> + </a> + </div> + <div class="col"> + <div class="bg-secondary text-light p-2">{{ (productConfig | async) | json}}</div> + </div> + </div> </div> <div *ngIf="mergedConfig | async"> diff --git a/src/app/products/product/product-details/product-prerequsites/product-prerequsites.component.html b/src/app/products/product/product-details/product-prerequsites/product-prerequsites.component.html index 05768072ed8da58b21c4001a961fa259174b1247..7371222f0e05f78602588f525ab04c54fe39f530 100644 --- a/src/app/products/product/product-details/product-prerequsites/product-prerequsites.component.html +++ b/src/app/products/product/product-details/product-prerequsites/product-prerequsites.component.html @@ -3,7 +3,7 @@ <span class="badge badge-light border">{{pre.name}}</span> </div> <div class="col"> - <a [routerLink]="['/products']" [queryParams]="{pattern: pre.requiredProduct.id}"> + <a [routerLink]="['/products']" [queryParams]="{pattern: pre.requiredProduct.id, type: pre.name}"> {{pre.requiredProduct.name }} </a> <button type="button" class="btn btn-xs btn-outline-primary border-0 ml-2" (click)="showDetails = !showDetails"> diff --git a/src/app/products/product/product-details/product-version/product-version.component.html b/src/app/products/product/product-details/product-version/product-version.component.html index f67bf97c9704757ec01b0525f9060a00aa2b89a9..cff86d4c990b9505f31f49d2da8421653fca691f 100644 --- a/src/app/products/product/product-details/product-version/product-version.component.html +++ b/src/app/products/product/product-details/product-version/product-version.component.html @@ -5,7 +5,7 @@ <div class="col"> <ng-container *ngFor="let js of (version.jobSpecifications | keyvalue)"> <span class="badge badge-light border mr-1">Job</span> - <a [routerLink]="['/jobs']" [queryParams]="{pattern: js.key}">{{js.value.name}}</a> + <a [routerLink]="['/jobs']" [queryParams]="{pattern: js.key, type: js.value.queue}">{{js.value.name}}</a> <button type="button" class="btn btn-xs btn-outline-primary border-0 ml-2" (click)="toggleDetails(js.key.toString())"> <fa-icon [icon]="faCaretDown" *ngIf="!showDetails"></fa-icon> diff --git a/src/app/products/products.component.html b/src/app/products/products.component.html index 21c321e719c9804eae3476df4ef8c21706ce0679..f175a9802ab52994f0389d9ff40d1621738d3d72 100644 --- a/src/app/products/products.component.html +++ b/src/app/products/products.component.html @@ -23,17 +23,38 @@ </ul> </div> - <form class="form-inline col pl-4" [formGroup]="formGroup"> + <form class="form-inline col-auto pl-4" [formGroup]="formGroup"> <div class="form-group"> <input type="text" class="form-control" id="pattern" placeholder="Pattern/Id" formControlName="pattern"> </div> </form> + + <div class="col-auto pl-3"><b>Sort By</b>:</div> + <div class="btn-group col-auto" ngbDropdown> + <button class="btn btn-light btn-sm" type="button" id="sort-select" ngbDropdownToggle> + {{ getPrettyName(filters['PRODUCT_SORT']) }}</button> + <ul ngbDropdownMenu> + <li ngbDropdownItem *ngFor="let col of sortColumns"> + <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setSortColumn(col)">{{getPrettyName(col)}}</button> + </li> + </ul> + </div> + <div class="btn-group col-auto" ngbDropdown> + <button class="btn btn-light btn-sm" type="button" id="sort-direction" ngbDropdownToggle> + {{ filters['SORT_DIRECTION'] }}</button> + <ul ngbDropdownMenu> + <li ngbDropdownItem *ngFor="let direction of sortDirections"> + <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setSortDirection(direction)">{{direction}}</button> + </li> + </ul> + </div> </div> </div> <div class="col"> <p class="text-danger text-right p-2 m-0">{{ ((currentPage - 1) * resultsPerPage) + 1 }} - - {{ currentPage * resultsPerPage}} of {{ numResults }}</p> + - {{ currentPage * resultsPerPage < numResults ? currentPage * resultsPerPage : numResults}} + of {{ numResults }}</p> </div> <div class="col-auto"> @@ -103,6 +124,7 @@ <label class="control-label" for="sbid">Enter OPT SB ID:</label> <input type="number" class="form-control" name="sbid" id="sbid" placeholder="36158915" formControlName="sbid"/> + <div class="text-danger" *ngIf="!productsbId.pristine && !productsbId.valid">SB ID can only be digits</div> </div> </div> <div class="col-xs-12 col-md-6"> @@ -127,12 +149,14 @@ <div class="row no-gutters mx-1"> <div class="col-1 px-2 py-3 thead_th">Id</div> <div class="col px-2 py-3 thead_th">Name</div> - <div class="col px-2 py-3 thead_th" *ngIf="showImageDetails">Phase Center (deg) <span class="ml-3">Image Size (deg)</span></div> + <div class="col px-2 py-3 thead_th" *ngIf="showImageDetails">Phase Center (deg) <span + class="ml-3">Image Size (deg)</span></div> <div class="col-auto px-2 py-3 thead_th">Status</div> <div class="col-auto px-2 py-3 thead_th" *ngIf="canDeleteProducts"> </div> </div> <ng-container *ngFor="let product of products"> - <app-product [product]="product" [canDeleteProducts]="canDeleteProducts" (refreshProducts)="getProducts(currentPage)"></app-product> + <app-product [product]="product" [canDeleteProducts]="canDeleteProducts" + (refreshProducts)="getPageInfoAndProduct()"></app-product> </ng-container> </ng-container> diff --git a/src/app/products/products.component.ts b/src/app/products/products.component.ts index f9b3146d9704d0136db80b7969f74221ab3672b2..005ade615501f0e67858fbc1b35488b1b403c62c 100644 --- a/src/app/products/products.component.ts +++ b/src/app/products/products.component.ts @@ -34,6 +34,8 @@ export class ProductsComponent implements OnInit, OnDestroy { private filters$: Subscription; public filters: any; + public sortColumns: Array<string>; + public sortDirections: Array<string>; private readonly settings$: Subscription; public canDeleteProducts = false; @@ -81,13 +83,15 @@ export class ProductsComponent implements OnInit, OnDestroy { }, error => { this.canDeleteProducts = false; }); + this.sortColumns = this.filterService.getFilter('PRODUCT_SORT'); + this.sortDirections = this.filterService.getFilter('SORT_DIRECTION'); } ngOnInit() { this.productFormGroup = new FormGroup({ epoch: new FormControl(null, Validators.required), sbname: new FormControl(null, {validators: Validators.required, updateOn: "blur"}), - sbid: new FormControl(null, [Validators.required, Validators.pattern('^[0-9]+$')]), + sbid: new FormControl(null, [Validators.required, Validators.min(1), Validators.max(999999999)]), minitiles: new FormControl(null, Validators.required) }); @@ -109,7 +113,7 @@ export class ProductsComponent implements OnInit, OnDestroy { if (params.hasOwnProperty('type')) { const paramType = Product.getTypeFromName(params.type); if (paramType) { - this.filterService.setCurrentSetting('PRODUCT_TYPES', paramType); + this.filterService.setCurrentSetting('PRODUCT_TYPE', paramType); } } if (params.hasOwnProperty('epoch')) { @@ -162,6 +166,18 @@ export class ProductsComponent implements OnInit, OnDestroy { this.filterService.setCurrentSetting('PRODUCT_STATUS', status); } + setSortColumn(column: string): void { + this.filterService.setCurrentSetting('PRODUCT_SORT', column); + } + + setSortDirection(direction: string): void { + this.filterService.setCurrentSetting('SORT_DIRECTION', direction); + } + + getPrettyName(name: string): string { + return FiltersService.prettyName(name); + } + goToPage(page: number): boolean { if (page < 1) { page = 1; @@ -173,7 +189,7 @@ export class ProductsComponent implements OnInit, OnDestroy { return false; } this.currentPage = page; - this.getProducts(page); + this.getProducts(); } getPages(): Array<any> { @@ -186,12 +202,17 @@ export class ProductsComponent implements OnInit, OnDestroy { this.pages$ = this.productService.getRecordCount(epoch.id, type.id, this.pattern).subscribe((productNumber: number) => { this.numResults = productNumber; this.pages = Math.ceil(productNumber / this.resultsPerPage); - this.currentPage = 1; - this.getProducts(1); + if (this.currentPage > this.pages) { + this.currentPage = this.pages; + } + if (this.currentPage < 1) { + this.currentPage = 1; + } + this.getProducts(); }); } - getProducts(pageId: number): void { + getProducts(): void { // clear a previous call if (this.products$) { this.products$.unsubscribe(); @@ -200,7 +221,7 @@ export class ProductsComponent implements OnInit, OnDestroy { const epoch = this.filterService.getCurrentSetting('EPOCH'); const type = this.filterService.getCurrentSetting('PRODUCT_TYPE'); this.alertService.info('Getting ' + type.label); - this.products$ = this.productService.getPage(epoch.id, type.id, pageId - 1, this.pattern).subscribe((products: Array<Product>) => { + this.products$ = this.productService.getPage(epoch.id, type.id, this.currentPage - 1, this.pattern).subscribe((products: Array<Product>) => { if (products && products.length > 0) { this.alertService.success(type.label + ' loaded.'); this.products = products; @@ -228,7 +249,7 @@ export class ProductsComponent implements OnInit, OnDestroy { let possible_id = parseInt(this.pattern); if (isNaN(possible_id)) { this.getPageCount(); - this.getProducts(1); + this.getProducts(); } else { this.getProductById(possible_id.toString()); } @@ -247,17 +268,38 @@ export class ProductsComponent implements OnInit, OnDestroy { generateProduct() { this.alertService.info('Generating products...'); - this.schedblockService.createSchedblock( - this.productFormGroup.get('epoch').value, - this.productFormGroup.get('sbname').value, - this.productFormGroup.get('sbid').value, - this.productFormGroup.get('minitiles').value - ).subscribe(response => { - this.alertService.success('Products Generated'); - this.getProducts(this.currentPage); - }, (error) => { - this.alertService.error('Product generating failed'); - }); + if (this.productFormGroup.valid) { + this.schedblockService.createSchedblock( + this.productEpoch.value, + this.productSBname.value, + this.productsbId.value, + this.productMinitiles.value + ).subscribe(response => { + this.alertService.success('Products Generated'); + this.getProducts(); + }, (error) => { + this.alertService.error('Product generating failed'); + }); + } else { + this.alertService.error('Invalid values in the form'); + console.log(this.productFormGroup.value); + } + } + + /** + * conveinence getters for the form + */ + get productEpoch(): FormControl { + return this.productFormGroup.get('epoch') as FormControl; + } + get productSBname(): FormControl { + return this.productFormGroup.get('sbname') as FormControl; + } + get productsbId(): FormControl { + return this.productFormGroup.get('sbid') as FormControl; + } + get productMinitiles(): FormControl { + return this.productFormGroup.get('minitiles') as FormControl; } ngOnDestroy(): void { diff --git a/src/app/services/filters.service.ts b/src/app/services/filters.service.ts index 4d29071f3c3aa6d9711b57539350833cd0fbd9a0..ff08a17b278f87c3c2fa54d866279fb9149b3b16 100644 --- a/src/app/services/filters.service.ts +++ b/src/app/services/filters.service.ts @@ -1,9 +1,10 @@ import {Injectable} from '@angular/core'; import {Product} from "../model/product"; -import {Job} from "../model/job"; +import {Job, JobSpec} from "../model/job"; import {BehaviorSubject, Observable} from "rxjs"; import {StorageService} from "./storage.service"; import {Epoch} from "../model/epoch"; +import {Tile} from "../model/tile"; @Injectable({ @@ -16,7 +17,12 @@ export class FiltersService { 'PRODUCT_TYPE': Product.TYPES, 'PRODUCT_STATUS': ['ALL', 'COMPLETED', 'WAITING', 'READY', 'COMPLETED', 'PROCESSING', 'CACHED'], 'JOB_QUEUE': Job.QUEUES, - 'JOB_STATUS': ['ALL', 'WAITING', 'PROCESSING', 'QA_READY', 'QA_MANUAL', 'QA_ACCEPTED', 'QA_REJECTED', 'QA_ON_HOLD', 'QA_MANUALLY_ACCEPTED', 'ERROR'] + 'JOB_STATUS': ['ALL', 'WAITING', 'PROCESSING', 'QA_READY', 'QA_MANUAL', 'QA_ACCEPTED', 'QA_REJECTED', 'QA_ON_HOLD', 'QA_MANUAL_ACCEPTED', 'ERROR'], + 'SORT_DIRECTION': ['ASC', 'DESC'], + 'TILE_SORT': Tile.SORT_COLUMNS, + 'PRODUCT_SORT': Product.SORT_COLUMNS, + 'JOBSPEC_SORT': JobSpec.SORT_COLUMNS, + 'JOB_SORT': Job.SORT_COLUMNS }; private defaultSettings = { @@ -24,7 +30,12 @@ export class FiltersService { 'PRODUCT_TYPE': Product.getTypeFromName('calibration'), 'PRODUCT_STATUS': 'ALL', 'JOB_QUEUE': Job.getQueueFromName('calibration'), - 'JOB_STATUS': 'ALL' + 'JOB_STATUS': 'ALL', + 'SORT_DIRECTION': 'ASC', + 'TILE_SORT': 'id', + 'PRODUCT_SORT': 'id', + 'JOBSPEC_SORT': 'id', + 'JOB_SORT': 'id' }; private currentSettings = {}; @@ -55,6 +66,15 @@ export class FiltersService { this._currentSettings.next(this.currentSettings); } + static prettyName(name: string): string { + if (name.length < 3) { + return name.toUpperCase(); + } else { + name = name.replace(/_/g, ' '); + return name.toLowerCase().split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '); + } + } + getFilters() { return this.filters } diff --git a/src/app/services/jobs.service.ts b/src/app/services/jobs.service.ts index 83588857ff62f64bc3bed1c2928d361a76b153c0..97bf2c47c3acf027b9b8d80e174044b0d5a4297a 100644 --- a/src/app/services/jobs.service.ts +++ b/src/app/services/jobs.service.ts @@ -1,9 +1,11 @@ import {Injectable} from '@angular/core'; -import {HttpClient, HttpResponse} from "@angular/common/http"; +import {HttpClient, HttpParams} from "@angular/common/http"; import {ConfigurationService} from "../env/configuration.service"; import {Observable} from "rxjs"; import {map, switchMap} from "rxjs/operators"; import {Job, JobExecution, JobSpec} from "../model/job"; +import {FiltersService} from "./filters.service"; +import {CustomHttpParamEncoder} from "../custom-http-param-encoder"; @Injectable({ providedIn: 'root' @@ -12,42 +14,69 @@ export class JobsService { endPoint: string = '/services/job/'; - constructor(private http: HttpClient, private configService: ConfigurationService) { - } - - public getJobs(queue: string, id: string, pattern: string, status: string): Observable<Array<Job>> { - return this.http.get<Array<Job>>(this.configService.config.url + this.endPoint + 'optimized/queues/' + queue, {observe: 'response'}).pipe( - map((response: HttpResponse<Array<Job>>) => { - let reply = response.body; - if (id && id.length > 0) { - reply = reply.filter(job => job.job_id.toString() === id); - } else { - if (pattern && pattern.length > 0) { - reply = reply.filter(job => job.job_name.match(pattern.replace("+", "\\+"))); - } - if (status && status.length > 0 && status !== 'ALL') { - reply = reply.filter(job => job.job_status == status); - } - } - return reply; + constructor( + private http: HttpClient, + private configService: ConfigurationService, + private filtersService: FiltersService) { + } + + public getJobRecordCount(epoch: number, queue: string, pattern: string, status: string): Observable<number> { + return this.http.get<number>(this.configService.config.url + this.endPoint + 'byEpoch/' + epoch.toString() + '/byQueue/' + queue + + '/pages?pattern=' + encodeURIComponent(pattern) + '&status=' + encodeURI(status), {observe: 'response'}).pipe( + map(response => { + return response.body; })); } - public getJobSpecs(queue: string, id: string, pattern: string, status: string): Observable<Array<Job>> { - return this.http.get<Array<Job>>(this.configService.config.url + this.endPoint + 'optimized/queues/' + queue, {observe: 'response'}).pipe( - map((response: HttpResponse<Array<Job>>) => { - let reply = response.body; - if (id && id.length > 0) { - reply = reply.filter(job => job.jobspec_id.toString() === id); - } else { - if (pattern && pattern.length > 0) { - reply = reply.filter(job => job.jobspec_name.match(pattern.replace("+", "\\+"))); - } - if (status && status.length > 0 && status !== 'ALL') { - reply = reply.filter(job => job.job_status == status); - } - } - return reply; + public getJobSpecRecordCount(epoch: number, queue: string, pattern: string, status: string): Observable<number> { + return this.http.get<number>(this.configService.config.url + this.endPoint + 'specs/byEpoch/' + epoch.toString() + '/byQueue/' + queue + + '/pages?pattern=' + encodeURIComponent(pattern) + '&status=' + encodeURI(status), {observe: 'response'}).pipe( + map(response => { + return response.body; + })); + } + + public getJobSpecPage(epoch: number, queue: string, pageId: number, pattern: string, status: string): Observable<Array<JobSpec>> { + let params = new HttpParams({encoder: new CustomHttpParamEncoder()}); + if (!!pattern) { + params = params.append('pattern', pattern); + } + if (!!status) { + params = params.append('status', status); + } + + const sortName = this.filtersService.getCurrentSetting('JOBSPEC_SORT'); + const sortDir = this.filtersService.getCurrentSetting('SORT_DIRECTION'); + + params = params.append('columnNames', sortName); + params = params.append('directions', sortDir); + + return this.http.get<Array<JobSpec>>(this.configService.config.url + this.endPoint + 'specs/byEpoch/' + epoch.toString() + '/byQueue/' + queue + + '/pages/' + pageId.toString() + '/', {params: params, observe: 'response'}).pipe( + map(response => { + return response.body; + })); + } + + public getJobPage(epoch: number, queue: string, pageId: number, pattern: string, status: string): Observable<Array<Job>> { + let params = new HttpParams({encoder: new CustomHttpParamEncoder()}); + if (!!pattern) { + params = params.append('pattern', pattern); + } + if (!!status) { + params = params.append('status', status); + } + + const sortName = this.filtersService.getCurrentSetting('JOB_SORT'); + const sortDir = this.filtersService.getCurrentSetting('SORT_DIRECTION'); + + params = params.append('columnNames', sortName); + params = params.append('directions', sortDir); + + return this.http.get<Array<Job>>(this.configService.config.url + this.endPoint + 'byEpoch/' + epoch.toString() + '/byQueue/' + queue + + '/pages/' + pageId.toString() + '/', {params: params, observe: 'response'}).pipe( + map(response => { + return response.body; })); } @@ -58,6 +87,14 @@ export class JobsService { })); } + // unlike the above, this returns an object the same shape as the pages + public getJobSpecById(id: number): Observable<JobSpec> { + return this.http.get<JobSpec>(this.configService.config.url + this.endPoint + 'specs/byId/' + id, {observe: 'response'}).pipe( + map(response => { + return response.body; + })); + } + public jobSpecToJob(spec: JobSpec): Job { const summary = new Job(); summary.jobspec_id = spec.id; @@ -80,13 +117,22 @@ export class JobsService { return summary; } - public getJob(id: number): Observable<any> { + + public getJobExecution(id: number): Observable<JobExecution> { return this.http.get<JobExecution>(this.configService.config.url + this.endPoint + 'jobs/' + id, {observe: 'response'}).pipe( map(response => { return response.body; })); } + // unlike the above, this returns an object the same shape as the pages + public getJobById(id: number): Observable<Job> { + return this.http.get<Job>(this.configService.config.url + this.endPoint + 'byId/' + id, {observe: 'response'}).pipe( + map(response => { + return response.body; + })); + } + public createJob(id: number, queue: string, version: number): Observable<any> { let command = {productId: id, inputProductVersions: [version]}; return this.http.post(this.configService.config.url + this.endPoint + 'createJob?id=' + id + '&queue=' + queue, command, {observe: "response"}).pipe( diff --git a/src/app/services/products.service.ts b/src/app/services/products.service.ts index a327813763f5c56d84c44a88afdbff737b8df7f6..8e0a3c8134c368c3451ac0a1a6f040a4c0feb973 100644 --- a/src/app/services/products.service.ts +++ b/src/app/services/products.service.ts @@ -4,6 +4,8 @@ import {ConfigurationService} from "../env/configuration.service"; import {Observable} from "rxjs"; import {map} from "rxjs/operators"; import {Product} from "../model/product"; +import {FiltersService} from "./filters.service"; +import {CustomHttpParamEncoder} from "../custom-http-param-encoder"; @Injectable({ providedIn: 'root' @@ -12,22 +14,32 @@ export class ProductsService { endPoint: string = '/services/product/'; - constructor(private http: HttpClient, private configService: ConfigurationService) { + constructor( + private http: HttpClient, + private configService: ConfigurationService, + private filtersService: FiltersService) { } public getRecordCount(epoch: number, type: number, pattern: string): Observable<number> { return this.http.get<number>(this.configService.config.url + this.endPoint + 'byEpoch/' + epoch.toString() + '/byType/' + type.toString() + - '/pages?pattern=' + encodeURI(pattern), {observe: 'response'}).pipe( + '/pages?pattern=' + encodeURIComponent(pattern), {observe: 'response'}).pipe( map(response => { return response.body; })); } public getPage(epoch: number, type: number, pageId: number, pattern: string): Observable<Array<Product>> { - let params = new HttpParams(); + let params = new HttpParams({encoder: new CustomHttpParamEncoder()}); if (pattern) { params = params.append('pattern', pattern); } + + const sortName = this.filtersService.getCurrentSetting('PRODUCT_SORT'); + const sortDir = this.filtersService.getCurrentSetting('SORT_DIRECTION'); + + params = params.append('columnNames', sortName); + params = params.append('directions', sortDir); + return this.http.get<Array<Product>>(this.configService.config.url + this.endPoint + 'byEpoch/' + epoch.toString() + '/byType/' + type.toString() + '/pages/' + pageId.toString() + '/', {params: params, observe: 'response'}).pipe( map(response => { diff --git a/src/app/services/tiles.service.ts b/src/app/services/tiles.service.ts index d8a123290a9d5f783fb1548bda5814aaf95977d9..fdede641e3b80120b8314ebcccde8a4d6df63a1a 100644 --- a/src/app/services/tiles.service.ts +++ b/src/app/services/tiles.service.ts @@ -4,6 +4,7 @@ import {Observable} from "rxjs"; import {ConfigurationService} from "../env/configuration.service"; import {map} from "rxjs/operators"; import {Tile, TileDefinition} from "../model/tile"; +import {FiltersService} from "./filters.service"; @Injectable({ providedIn: 'root' @@ -12,13 +13,21 @@ export class TilesService { endPoint: string = '/services/minitiles/'; - constructor(private configService: ConfigurationService, private http: HttpClient) { + constructor( + private configService: ConfigurationService, + private http: HttpClient, + private filterService: FiltersService) { } public getTilesForEpoch(pattern: string, epoch: number): Observable<Array<Tile>> { let params = new HttpParams(); + const sortName = this.filterService.getCurrentSetting('TILE_SORT'); + const sortDir = this.filterService.getCurrentSetting('SORT_DIRECTION'); + params = params.append('pattern', pattern); + params = params.append('columnNames', sortName); + params = params.append('directions', sortDir); return this.http.get<Array<Tile>>(this.configService.config.url + this.endPoint + 'summary/epoch/' + epoch.toString(), { observe: 'response', @@ -28,6 +37,13 @@ export class TilesService { })); } + public updateTileSummary(epoch: number): Observable<any> { + return this.http.put<any>(this.configService.config.url + this.endPoint + 'summary/update?epoch=' + epoch.toString(), {}, {observe: 'response'}) + .pipe(map(response => { + return response.body; + })); + } + public getTileDefinition(id: number): Observable<TileDefinition> { return this.http.get<TileDefinition>(this.configService.config.url + this.endPoint + id + '/definitions', {observe: 'response'}).pipe(map(response => { diff --git a/src/app/tiles/tiles.component.html b/src/app/tiles/tiles.component.html index 57e6ec197c693ed73b4a56972cbcb024136b66a1..8e5c2c8898baf17e03e4077b3f180794b0f33a13 100644 --- a/src/app/tiles/tiles.component.html +++ b/src/app/tiles/tiles.component.html @@ -8,17 +8,17 @@ {{ getEpochName(epoch) }}</button> <ul ngbDropdownMenu> <li ngbDropdownItem> - <button type="button" class="btn btn-link p-0" (click)="setEpoch(-1)">Tests</button> + <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setEpoch(-1)">Tests</button> </li> <li ngbDropdownItem> - <button type="button" class="btn btn-link p-0" (click)="setEpoch(0)">Pilot</button> + <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setEpoch(0)">Pilot</button> </li> <li ngbDropdownItem> - <button type="button" class="btn btn-link p-0" (click)="setEpoch(1)">Epoch 1</button> + <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setEpoch(1)">Epoch 1</button> </li> <li ngbDropdownItem> - <button type="button" class="btn btn-link p-0" (click)="setEpoch(2)">Epoch 2</button> - </li> + <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setEpoch(2)">Epoch 2</button> + </li> </ul> </div> <form (ngSubmit)="getMinitilesForEpoch()" class="form-inline col-auto pl-4" [formGroup]="formGroup"> @@ -26,13 +26,33 @@ <input type="text" class="form-control" id="pattern" placeholder="Pattern" formControlName="pattern"> </div> </form> + + <div class="col-auto pl-3"><b>Sort By</b>:</div> + <div class="btn-group col" ngbDropdown> + <button class="btn btn-light btn-sm" type="button" id="sort-select" ngbDropdownToggle> + {{ getPrettyName(filters['TILE_SORT']) }}</button> + <ul ngbDropdownMenu> + <li ngbDropdownItem *ngFor="let col of sortColumns"> + <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setSortColumn(col)">{{getPrettyName(col)}}</button> + </li> + </ul> + </div> + <div class="btn-group col" ngbDropdown> + <button class="btn btn-light btn-sm" type="button" id="sort-direction" ngbDropdownToggle> + {{ filters['SORT_DIRECTION'] }}</button> + <ul ngbDropdownMenu> + <li ngbDropdownItem *ngFor="let direction of sortDirections"> + <button type="button" class="btn btn-link w-100 p-0 text-left" (click)="setSortDirection(direction)">{{direction}}</button> + </li> + </ul> + </div> </div> </div> <div class="col"></div> <div class="col-auto text-right"> - <button type="button" class="btn btn-danger btn-sm" (click)="getMinitilesForEpoch()"> + <button type="button" class="btn btn-danger btn-sm" (click)="updateSummary()"> <fa-icon [icon]="faSyncAlt"></fa-icon> - Refresh + Update </button> </div> </div> diff --git a/src/app/tiles/tiles.component.ts b/src/app/tiles/tiles.component.ts index efde0d218d062bca0a9a82e1a56917edfef2a602..8c8fd76399bcc3252e48c1f72a0a7f5a86b34b38 100644 --- a/src/app/tiles/tiles.component.ts +++ b/src/app/tiles/tiles.component.ts @@ -6,6 +6,7 @@ import {Tile} from "../model/tile"; import {FormControl, FormGroup} from "@angular/forms"; import {AlertService} from "../services/alert.service"; import {faSyncAlt} from "@fortawesome/free-solid-svg-icons"; +import {FiltersService} from "../services/filters.service"; @Component({ selector: 'app-tiles', @@ -23,7 +24,23 @@ export class TilesComponent implements OnInit, OnDestroy { public faSyncAlt = faSyncAlt; - constructor(private tileService: TilesService, private alertService: AlertService) { + private readonly filters$: Subscription; + public filters: any; + public sortColumns: Array<string>; + public sortDirections: Array<string>; + + + constructor( + private tileService: TilesService, + private alertService: AlertService, + private filtersService: FiltersService) { + this.sortColumns = this.filtersService.getFilter('TILE_SORT'); + this.sortDirections = this.filtersService.getFilter('SORT_DIRECTION'); + + this.filters$ = this.filtersService.currentSettings$.subscribe((filters: object) => { + this.filters = filters; + this.getMinitilesForEpoch(); + }); } ngOnInit() { @@ -48,6 +65,18 @@ export class TilesComponent implements OnInit, OnDestroy { this.getMinitilesForEpoch(); } + setSortColumn(column: string): void { + this.filtersService.setCurrentSetting('TILE_SORT', column); + } + + setSortDirection(direction: string): void { + this.filtersService.setCurrentSetting('SORT_DIRECTION', direction); + } + + getPrettyName(name: string): string { + return FiltersService.prettyName(name); + } + getEpochName(epoch: number): string { switch (epoch) { case -1: @@ -59,9 +88,18 @@ export class TilesComponent implements OnInit, OnDestroy { } } + updateSummary(): void { + this.alertService.info('Updating tile summary'); + this.tileService.updateTileSummary(this.epoch).subscribe(() => { + this.getMinitilesForEpoch(); + }, error => { + this.alertService.error('Tile summary update failed'); + }); + } + getMinitilesForEpoch() { - this.tiles = null; this.alertService.info('Getting Tiles'); + this.tiles = null; this.tiles$ = this.tileService.getTilesForEpoch(this.pattern, this.epoch).subscribe((mt: Array<Tile>) => { if (mt && mt.length > 0) { this.alertService.success('Tiles retrieved'); @@ -70,12 +108,16 @@ export class TilesComponent implements OnInit, OnDestroy { this.alertService.error('Tiles could not be retrieved'); } }); + } ngOnDestroy(): void { if (this.tiles$) { this.tiles$.unsubscribe(); } + if (this.filters$) { + this.filters$.unsubscribe(); + } } } diff --git a/src/env.js b/src/env.js index 0c7cf5ac0306b82a506282640b0ae838d88ee6ab..0e01b750538fe2172f68303cd328737c780a5183 100644 --- a/src/env.js +++ b/src/env.js @@ -13,6 +13,9 @@ A service in angular will capture this info and add it to a service case 'archive-new.nrao.edu': window.__env.configUrl = 'https://archive-new.nrao.edu/VlassMngr/services/configuration'; break; + case 'localhost': + window.__env.configUrl = 'http://localhost:8080/VlassMngr/services/configuration'; + break; default: window.__env.configUrl = 'https://webtest.aoc.nrao.edu/VlassMngr/services/configuration'; break; diff --git a/src/index.html b/src/index.html index 9cf72a44c6eb93d532ae6af14690034da956c6a1..f5a4e74f659dfac66a9ef81e30dc9a2bb2bd371c 100644 --- a/src/index.html +++ b/src/index.html @@ -2,7 +2,7 @@ <html lang="en"> <head> <meta charset="utf-8"> - <title>VlassMngr2</title> + <title>VLASS Manager</title> <base href="./"> <meta name="viewport" content="width=device-width, initial-scale=1">