diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index bc5e5a36f66dbf3e904bc6d61f71ed35be10c126..dcd7bead93b787dcf551cfe9ce5eb02b4d32a3f3 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -1,14 +1,21 @@ import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { LauncherComponent } from './launcher/launcher.component'; -import { TokenextractorComponent } from './tokenextractor/tokenextractor.component'; +import { KeygenComponent } from './keygen/keygen.component'; +import { ConnectingComponent } from './connecting/connecting.component'; + +// import { TokenextractorComponent } from './tokenextractor/tokenextractor.component'; const routes: Routes = [ { path: '', redirectTo: 'launch', pathMatch: 'full'}, { path: 'launch', component: LauncherComponent}, - // { path: 'sshauthz_callback', component: TokenextractorComponent} - { path: 'sshauthz_callback', component: LauncherComponent} + { path: 'finishlaunch', component: LauncherComponent}, + { path: 'cancellaunch', component: LauncherComponent}, + { path: 'sshauthz_callback', component: KeygenComponent}, + { path: 'connecting', component: ConnectingComponent } + + // { path: 'sshauthz_callback', component: LauncherComponent} ]; diff --git a/src/app/app.component.css b/src/app/app.component.css index b7da323b68967dd24667e48616df31db78844166..31ef901f27c3196c82b8461d129518631f654d33 100644 --- a/src/app/app.component.css +++ b/src/app/app.component.css @@ -3,3 +3,8 @@ Every toolbar row uses a flexbox row layout. */ flex: 1 1 auto; } +.fill-horizontal-space { + /* This fills the remaining space, by using flexbox. + Every toolbar row uses a flexbox row layout. */ + flex: 0 1 auto; +} diff --git a/src/app/app.component.html b/src/app/app.component.html index 19425a3eab26a12d81c60763b36691f42b1b0b43..11d6bd56902f676bd2b653864c579610f669d08b 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,14 +1,5 @@ -<mat-toolbar color="primary"> - <mat-toolbar-row> - <span>{{title}}</span> - <span class="fill-remaining-space"></span> - <span class="fill-remaining-space"></span> - <!-- <app-siteselection></app-siteselection> --> - <!-- <button mat-button *ngIf="authorised" (click)="logout()">Logout</button> - <button mat-button *ngIf="!authorised" (click)="login()">Login</button> --> - </mat-toolbar-row> - </mat-toolbar> -<router-outlet></router-outlet> -<app-teserrors></app-teserrors> +<router-outlet class="fill-remaining-space"></router-outlet> + +<!-- <app-teserrors></app-teserrors> --> diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 51a81f29de18c93fcf2910491fb43fbcccafc9be..44d79aa300b303e1e04ad250b5a39655e7dfb43d 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -15,20 +15,31 @@ import { MatTableModule } from '@angular/material/table'; import { MatCardModule } from '@angular/material'; import { MatToolbarModule } from '@angular/material'; import { MatDialogModule, MatDialog } from '@angular/material'; +import { MatSnackBarModule } from "@angular/material"; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ComputesitesService } from './computesites.service'; import { StrudelappsService } from './strudelapps.service'; +import { AuthorisationService } from './authorisation.service'; + import { TesService} from './tes.service'; import { FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { JobComponent } from './job/job.component'; -import { SiteselectionComponent } from './siteselection/siteselection.component'; +// import { SiteselectionComponent } from './siteselection/siteselection.component'; import { TeserrorsComponent } from './teserrors/teserrors.component'; import { AppRoutingModule } from './/app-routing.module'; import { TokenextractorComponent } from './tokenextractor/tokenextractor.component'; -import { LogindialogComponent } from './logindialog/logindialog.component'; +// import { LogindialogComponent } from './logindialog/logindialog.component'; import { LogoutdialogComponent } from './logoutdialog/logoutdialog.component'; +// import { LaunchdialogComponent } from './launchdialog/launchdialog.component'; + +import { ModaldialogComponent } from './modaldialog/modaldialog.component'; +import { MatSidenavModule, MatExpansionModule, MatIconModule } from '@angular/material'; +import { StrudelapplistComponent } from './strudelapplist/strudelapplist.component'; +import { KeygenComponent } from './keygen/keygen.component'; +import { ConnectingComponent } from './connecting/connecting.component'; + @@ -38,11 +49,15 @@ import { LogoutdialogComponent } from './logoutdialog/logoutdialog.component'; LauncherComponent, JoblistComponent, JobComponent, - SiteselectionComponent, + // SiteselectionComponent, TeserrorsComponent, TokenextractorComponent, - LogindialogComponent, - LogoutdialogComponent + LogoutdialogComponent, + ModaldialogComponent, + // LaunchdialogComponent, + StrudelapplistComponent, + KeygenComponent, + ConnectingComponent ], imports: [ BrowserModule, @@ -61,10 +76,14 @@ import { LogoutdialogComponent } from './logoutdialog/logoutdialog.component'; FormsModule, HttpClientModule, AppRoutingModule, + MatSidenavModule, + MatExpansionModule, + MatIconModule, + MatSnackBarModule, ], - entryComponents: [ LogindialogComponent, LogoutdialogComponent ], - providers: [ StrudelappsService, ComputesitesService, TesService, MatDialog], + entryComponents: [ ModaldialogComponent, LogoutdialogComponent, ], + providers: [ StrudelappsService, ComputesitesService, TesService, MatDialog, AuthorisationService], bootstrap: [AppComponent] }) export class AppModule { } diff --git a/src/app/authorisation.service.ts b/src/app/authorisation.service.ts index 017e383419c7478b7872123a0c135059a442abd4..51fa587faca021f49a1bbf46fd3e058b1fb50952 100644 --- a/src/app/authorisation.service.ts +++ b/src/app/authorisation.service.ts @@ -8,16 +8,24 @@ import {LocationStrategy} from '@angular/common'; // import { keypair } from 'keypair'; import * as keypair from 'keypair'; import * as forge from "node-forge"; -import { Identity, AuthToken, KeyCert, AuthService } from './identity'; +import { Identity, AuthToken, KeyCert, SshAuthzServer } from './identity'; import {BehaviorSubject} from 'rxjs/BehaviorSubject'; import {Subject} from 'rxjs/Subject'; import {TesService} from './tes.service'; +import { throwError } from 'rxjs'; + + +export class SshauthzServer {} @Injectable({ providedIn: 'root' }) export class AuthorisationService { - private token: Subject<AuthToken>; + // public token: BehaviorSubject<AuthToken>; + public token: AuthToken + public SshAuthzServers: BehaviorSubject<SshAuthzServer[]>; + public readyToNavigate: Subject<Boolean>; + public progress: Subject<string>; // private keyCert: Subject<KeyCert>; @@ -26,10 +34,30 @@ export class AuthorisationService { private route: ActivatedRoute, private router: Router, private tesService: TesService) { - this.token = new Subject<AuthToken>(); - this.token.subscribe(token => this.getCert(token)); + console.log('created AuthorisationService'); + // this.token = new BehaviorSubject<AuthToken>(new AuthToken('','')); + this.readyToNavigate = new Subject<Boolean>(); + this.readyToNavigate.next(false); + this.progress = new Subject<string>(); + this.progress.next(""); + // this.token.subscribe(token => this.getCert(token)); this.route.fragment.subscribe(frag => this.storeToken(frag)); + this.SshAuthzServers = new BehaviorSubject<SshAuthzServer[]>([]); + this.getSshAuthzServers(); + } + + getSshAuthzServers() { + let headers = new HttpHeaders(); + let options = { headers: headers, withCredentials: false}; + this.http.get<SshAuthzServer[]>('./assets/config/authservers.json',options) + .pipe(catchError(this.handleError('getSshAuthzServers'))) + .subscribe(resp => this.updateSshAuthzServers(resp)); } + + updateSshAuthzServers(resp) { + this.SshAuthzServers.next(<SshAuthzServer[]>resp); + } + storeToken(frag: string) { if (frag === undefined || frag == null) { return; @@ -37,8 +65,8 @@ export class AuthorisationService { let tokenmatch = null; let statematch = null; if (!(frag === undefined) && !(frag == null)) { - tokenmatch = frag.match(/access_token\=([\S\s]*?)[&|$]/); - statematch = frag.match(/state\=([\S\s]*?)[&|$]/); + tokenmatch = frag.match(/access_token\=([^&]+)(&|$)/); + statematch = frag.match(/state\=([^&]+)(&|$)/); } if (tokenmatch == null || statematch == null) { return; @@ -46,7 +74,7 @@ export class AuthorisationService { let accesstoken = tokenmatch[1]; let state = statematch[1]; - this.router.navigate(['/']); + // this.router.navigate(['/']); //Verify that the state matched the nonce we used when initiating login let tuple = JSON.parse(localStorage.getItem('authservice')); @@ -54,14 +82,13 @@ export class AuthorisationService { return } - this.token.next(new AuthToken(tokenmatch[1],tuple[0])); - - // TODO fire off a query to the auth service to get the associated sites + // this.token.next(new AuthToken(tokenmatch[1],tuple[0])); + this.token = new AuthToken(tokenmatch[1],tuple[0]); } - getCert(token: AuthToken) { + public getCert(token: AuthToken) { console.log('in getCert'); if (token.token === undefined || token.token === '' || token.token == null) { console.log('no authtoken available, we wont be able to generate a cert'); @@ -80,46 +107,65 @@ export class AuthorisationService { let headers = new HttpHeaders(); let options = { headers: headers, withCredentials: true}; - let data = {'token': token.token, 'pubkey': sshpub, 'signing_url': token.authservice.sign}; + let data = {'token': token.token, 'pubkey': sshpub, 'signing_url': token.sshauthzservice.sign}; console.log('posting to getcert',this.tesService.Base); this.http.post<any>(this.tesService.Base+'/getcert',data, options) .pipe(catchError(this.handleError('getCert',[]))) - .subscribe(resp => this.makeKeyCert(newkeypair.private, resp, token.authservice)) + .subscribe(resp => this.makeKeyCert(newkeypair.private, resp, token.sshauthzservice), + error => this.httperrorLogout(error,token.sshauthzservice)); console.log('getcert complete'); } - makeKeyCert(key: string, resp, authservice: AuthService) { + + makeKeyCert(key: string, resp, sshauthzservice: SshAuthzServer) { let keyCert = new KeyCert() keyCert.key = key; keyCert.cert = resp['cert']; - keyCert.authservice = authservice; console.log('updating keycert',keyCert); - this.tesService.keyCert.next(keyCert); + this.tesService.sshAdd(keyCert); + // this.tesService.keyCert.next(keyCert); + // As soon as the certificate has been generated, we log back out of the signing server + if (!(sshauthzservice.logout === null)) { + window.open(sshauthzservice.logout); + } + this.readyToNavigate.next(true); } - public login() { + public login(authservice: SshAuthzServer) { let redirect_uri = window.location.origin+this.locationStrategy.getBaseHref()+"sshauthz_callback"; let nonce="asdfzxcv"; - let authservice = new AuthService(); - authservice.base = "https://autht.massive.org.au/hpcid/"; - authservice.authorise = authservice.base + 'oauth/authorize'; - authservice.sign = authservice.base + 'api/v1/sign_key'; - authservice.client_id = "86c06039-e589-4b39-9d1f-9eca431be18f"; localStorage.setItem('authservice', JSON.stringify([authservice,nonce])); - window.location.assign(authservice.authorise+"?response_type=token&redirect_uri="+redirect_uri+"&state="+nonce+"&client_id="+authservice.client_id); + if (authservice.scope == null) { + window.location.assign(authservice.authorise+"?response_type=token&redirect_uri="+redirect_uri+"&state="+nonce+"&client_id="+authservice.client_id); + } else { + window.location.assign(authservice.authorise+"?response_type=token&redirect_uri="+redirect_uri+"&state="+nonce+"&client_id="+authservice.client_id+"&scope="+authservice.scope); + + } } private httperror(error: any) { + this.tesService.statusMsg.next(error); console.log(error); } + private httperrorLogout(error: any,sshauthzservice: SshAuthzServer) { + this.tesService.statusMsg.next(error); + console.log(error); + if (!(sshauthzservice.logout === null)) { + window.open(sshauthzservice.logout); + } + } private handleError<T> (operation = 'operation', result?: T) { return (error: any): Observable<T> => { console.log('in handle error',operation); console.log(error.status); console.error(error); - return of(result as T); + if (error.status == 500) { + return throwError("The authorisation server encountered and error. Please try again in a few minutes") + } + return throwError(error.message); + // return of(result as T); }; } } diff --git a/src/app/computesite.ts b/src/app/computesite.ts index a6de7f31281d5d7bc47f25b869330dcd4f4ae111..695f35545aad8b8d4bbab933a6ab507c019d037b 100644 --- a/src/app/computesite.ts +++ b/src/app/computesite.ts @@ -1,3 +1,5 @@ +import {BehaviorSubject} from 'rxjs/BehaviorSubject'; + export class Computesite { url: string; // The URL runs a web service to help construct sbatch commands // Infact the URL should return the entire interface for @@ -7,7 +9,9 @@ export class Computesite { cafingerprint: string; // Certificates contain a CA fingerprint. We use this // to figure out which compute site a certificate is valid // for - appCatalog: Strudelapp[]; + appCatalog: BehaviorSubject<Strudelapp[]>; + appCatalogUri: string; + } export class Strudelapp { diff --git a/src/app/computesites.service.ts b/src/app/computesites.service.ts index 219535ae8932eb8a2263ed4e2b57ddb6b8b27625..4577d558fb97fa8663a4d4456956edfa04c69e64 100644 --- a/src/app/computesites.service.ts +++ b/src/app/computesites.service.ts @@ -1,17 +1,67 @@ import { Injectable } from '@angular/core'; -import { Computesite } from './computesite'; -import { COMPUTESITES } from './mock-compute-site'; +import { Computesite, Strudelapp } from './computesite'; +import {BehaviorSubject} from 'rxjs/BehaviorSubject'; +import { HttpClientModule, HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; +import { Observable } from 'rxjs/Observable'; +import { of } from 'rxjs/observable/of'; +import { catchError, map, tap } from 'rxjs/operators'; + + @Injectable({ providedIn: 'root', }) export class ComputesitesService { + public computesites: BehaviorSubject<Computesite[]>; + + constructor(private http: HttpClient,) { + this.computesites = new BehaviorSubject<Computesite[]>([]); + this.computesites.subscribe(computesites => this.getStrudelApps(computesites)) + this.getComputeSites(); + } - constructor() { } + getStrudelApps(computesites: Computesite[]) { + for (let s of computesites) { + let headers = new HttpHeaders(); + let options = { headers: headers, withCredentials: false}; + this.http.get<Strudelapp[]>(s.appCatalogUri,options) + .pipe(catchError(this.handleError('getStrudelApps'))) + .subscribe(resp => this.updateStrudelApps(s.appCatalog,resp)); + } - getComputeSites(): Computesite[] { - return COMPUTESITES; } + private handleError<T> (operation = 'operation', result?: T) { + return (error: any): Observable<T> => { + console.error(error); + return of(result as T); + }; + } + + updateStrudelApps(appCatalog: BehaviorSubject<Strudelapp[]>,apps) { + appCatalog.next(<Strudelapp[]>apps); + } + + getComputeSites() { + let headers = new HttpHeaders(); + let options = { headers: headers, withCredentials: false}; + this.http.get<Strudelapp[]>('./assets/config/computesites.json',options) + .pipe(catchError(this.handleError('getComputeSites'))) + .subscribe(resp => this.updateComputeSites(resp)); + } + + updateComputeSites(resp) { + var computesites: Computesite[] = [] + for (let cs of resp) { + let computesite = <Computesite>cs; + computesite.appCatalog = new BehaviorSubject<Strudelapp[]>([]) + computesites.push(computesite); + } + this.computesites.next(computesites); + } + + returnComputeSites(): Computesite[] { + return this.computesites.value; + } } diff --git a/src/app/identity.ts b/src/app/identity.ts index 952370b7daa3aac0ee69baffe62ff09ab01dcc40..63a079b7e96690fbedc7a234f463ac52a61f1a4e 100644 --- a/src/app/identity.ts +++ b/src/app/identity.ts @@ -4,37 +4,52 @@ import {Computesite} from './computesite'; export class Identity { username: string; site: Computesite; - authservice: AuthService; + authservice: SshAuthzServer; constructor( username: string, site: Computesite) { this.username = username; this.site = site; } + + copy_skip_catalog(): Identity { + let id = new Identity(null,null); + id.username = this.username; + id.site = new Computesite(); + id.site.url = this.site.url; + id.site.host = this.site.host; + id.site.name = this.site.name; + id.site.cafingerprint = this.site.cafingerprint; + return id; + } displayName(): string { return this.username+'@'+this.site.name; } repr(): string { - return JSON.stringify([this.username,this.site.cafingerprint,this.site.host]); + return JSON.stringify([this.username,this.site.cafingerprint,this.site.host,this.site.url]); } } export class AuthToken { token: string; - authservice: AuthService; - constructor( token: string, authservice: AuthService ) { + sshauthzservice: SshAuthzServer; + constructor( token: string, sshauthzservice: SshAuthzServer ) { this.token = token; - this.authservice = authservice; + this.sshauthzservice = sshauthzservice; } } export class KeyCert { key: string; cert: string; - authservice: AuthService; + // authservice: SshAuthzServer; } -export class AuthService { +export class SshAuthzServer { base: string; authorise: string; sign: string; client_id: string; + name: string; + icon: string; + scope: string; + logout: string; } diff --git a/src/app/job/job.component.html b/src/app/job/job.component.html index 68466604262e97d37c032f36cf332bf2463fefde..2fbde39f3d096b478da7cd8abd5c29b2f9cfb845 100644 --- a/src/app/job/job.component.html +++ b/src/app/job/job.component.html @@ -9,9 +9,11 @@ {{ jobdata.state }} </td> <td withdt="10%"> - <button mat-button (click)="onCancel()"> - Cancel - </button> + <div *ngIf="!nocancel"> + <button mat-button (click)="onCancel()" > + Cancel + </button> + </div> </td> <td width="10%"> <div *ngIf="available"> diff --git a/src/app/job/job.component.ts b/src/app/job/job.component.ts index c1518a7895e6349e50531b9a9ec8362ee24d3936..0998980c1c0ec3e073a184bfb1f521c8b6da588a 100644 --- a/src/app/job/job.component.ts +++ b/src/app/job/job.component.ts @@ -13,33 +13,36 @@ export class JobComponent implements OnInit { @Input() jobdata: Job; public available: Boolean; private busy: Boolean; + public nocancel: Boolean; constructor(private tesService: TesService, private strudelAppsService: StrudelappsService) { } ngOnInit() { - console.log('creating job component'); if (this.jobdata.state == "RUNNING") { this.available = true; } else { this.available = false; } this.tesService.busy.subscribe(busy => this.busy = busy); - console.log('creating job component complete'); + if (this.jobdata.app.startscript === null) { + this.nocancel = true; + } else { + this.nocancel = false; + } } onCancel() { - this.jobdata.app = this.strudelAppsService.getApp(this.jobdata.name); + // this.jobdata.app = this.strudelAppsService.getApp(this.jobdata.name); console.log(this.jobdata); this.tesService.cancel(this.jobdata); } onConnect() { - console.log('attempting connect'); // Before connecting we must resolve what type of app we are connecting to - this.jobdata.app = this.strudelAppsService.getApp(this.jobdata.name); + // this.jobdata.app = this.strudelAppsService.getApp(this.jobdata.name); this.tesService.connect(this.jobdata); } diff --git a/src/app/joblist/joblist.component.css b/src/app/joblist/joblist.component.css index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0c6efabb438ac61b93bb5959301e4e760b7fe8d8 100644 --- a/src/app/joblist/joblist.component.css +++ b/src/app/joblist/joblist.component.css @@ -0,0 +1,9 @@ +html, body { + height: 100%; +} + +.wrapper { + /* display: flex; + flex-direction: column; */ + height: 100%; +} diff --git a/src/app/joblist/joblist.component.html b/src/app/joblist/joblist.component.html index 0ba48dc7f3b06feb2394db0dd5c6b2691cf58948..ac77ade48a30748d94588678be1eec298f5bfee1 100644 --- a/src/app/joblist/joblist.component.html +++ b/src/app/joblist/joblist.component.html @@ -1,16 +1,17 @@ -<div *ngIf="identities.length == 0"> +<!-- <div *ngIf="identities.length == 0"> <mat-card> Click identity and login to a service to start an application. </mat-card> </div> -<div *ngFor="let id of identities"> - <mat-card> - <mat-card-title>{{ id.displayName() }}</mat-card-title> - <div *ngIf="jobs[id.repr()] == undefined || jobs[id.repr()].length == 0 "> - No jobs running yet +<div *ngFor="let id of identities"> --> + + <mat-card class="fill-remaining-space"> + <div *ngIf="identity == undefined" > + Select your username an compute site on the left to see running jobs or start new jobs. + </div> + <div *ngIf="!(identity == undefined)"> + <div *ngFor="let job of jobs[identity.repr()]"> + <app-job [jobdata]=job></app-job> + </div> </div> - <div *ngFor="let job of jobs[id.repr()]"> - <app-job [jobdata]=job></app-job> - </div> </mat-card> -</div> diff --git a/src/app/joblist/joblist.component.ts b/src/app/joblist/joblist.component.ts index f27d908f909b05e591b51deb1789a4541c82611e..5f2d2f14cecf9f5c38a1f4d4905c5d80c4bde903 100644 --- a/src/app/joblist/joblist.component.ts +++ b/src/app/joblist/joblist.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, Input } from '@angular/core'; import {TesService} from '../tes.service'; import { Job } from '../job'; import { Observable } from 'rxjs/Observable'; @@ -11,21 +11,23 @@ import { Identity } from '../identity'; templateUrl: './joblist.component.html', styleUrls: ['./joblist.component.css'] }) + export class JoblistComponent implements OnInit { + @Input() identity: Identity; public jobs: {[id: string]: Job[] } = {}; public identities: Identity[]; private displayedColumns = ['id']; private jobsSubscription: any; private idSubscription: any; + constructor(private tesService: TesService,) { } ngOnInit() { - console.log('creating joblist component'); this.jobsSubscription = this.tesService.joblist.subscribe(jobs => this.updateJobs(jobs)); - this.idSubscription = this.tesService.identities.subscribe(ids => this.updateIds(ids)); - console.log("joblist component complete"); + // this.idSubscription = this.tesService.identities.subscribe(ids => this.updateIds(ids)); + } public ngOnDestroy(): void { @@ -34,18 +36,19 @@ export class JoblistComponent implements OnInit { } } - updateIds(identities: Identity[]) { - console.log('update identities'); - this.identities = identities; - console.log('update identities complete'); - - } + // updateIds(identities: Identity[]) { + // this.identities = identities; + // } updateJobs(jobs) { - console.log('updating jobs') + console.log('update jobs',jobs); this.jobs = jobs; - console.log('update jobs complete') + } + // setId(identity) { + // this.identity = identity; + // } + } diff --git a/src/app/launcher/launcher.component.css b/src/app/launcher/launcher.component.css index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..67e39868343ed94b7f23e6a1504f5879721f9e57 100644 --- a/src/app/launcher/launcher.component.css +++ b/src/app/launcher/launcher.component.css @@ -0,0 +1,10 @@ +.wrapper { + /* display: flex; + flex-direction: column; */ + flex: 1 1 auto; +} +.vspace { + /* display: flex; + flex-direction: column; */ +height: 500px; +} diff --git a/src/app/launcher/launcher.component.html b/src/app/launcher/launcher.component.html index 5ad7440af8fb65de1127d07f27c11bd53bf6ec7f..0315cba11f0811081f42b79d9499f4d6e8bab209 100644 --- a/src/app/launcher/launcher.component.html +++ b/src/app/launcher/launcher.component.html @@ -1,27 +1,47 @@ +<mat-toolbar color="primary"> + <mat-toolbar-row> + <button mat-icon-button (click)=idSideNav.toggle()><mat-icon>menu</mat-icon></button> -<mat-form-field> - <mat-select placeholder="Identity" (selectionChange)="selectId($event)"> - <mat-option *ngFor="let id of identities" [value]=id> - {{id.displayName()}} - </mat-option> - <mat-option> - <button mat-button (click)=login()>Login ... </button> - </mat-option> - <mat-option> - <button mat-button (click)=logout()> Logout </button> - </mat-option> + <span>Strudel v2.0</span> + <span class="fill-horizontal-space"></span> + </mat-toolbar-row> + </mat-toolbar> - </mat-select> -</mat-form-field> -<mat-form-field> - <mat-select placeholder="Application" [(value)]=app> - <mat-option *ngFor="let a of strudelapps" [value]=a> - {{a.name}} - </mat-option> - </mat-select> -</mat-form-field> -<button mat-button (click)=configureResources() [disabled]="!app || !identity">Configure Resources</button> -<!-- <button mat-button (click)=configureApp() [disabled]="!app || !identity">Configure App</button> --> -<button mat-button (click)=submitApp() [disabled]="!app || !identity">Start</button> +<mat-sidenav-container autosize class="fill-remaining-space"> + <mat-sidenav #idSideNav mode="side" opened> + <mat-accordion> + <mat-expansion-panel (click)=selectId(undefined)> + <mat-expansion-panel-header> + <mat-panel-title> + Login + </mat-panel-title> + </mat-expansion-panel-header> + <div *ngFor="let sshauthzserver of sshauthzservers"> + <button mat-button (click)=login(sshauthzserver)>Login to {{ sshauthzserver.name }} </button> + </div> + <button mat-button (click)=logout()>Logout</button> + </mat-expansion-panel> + <div *ngFor="let id of identities"> + <mat-expansion-panel> + <mat-expansion-panel-header> + {{ id.displayName() }} + </mat-expansion-panel-header> + <!-- <div *ngFor="let app of id.site.appCatalog.value"> + <div *ngIf="app.startscript != null"> + <button mat-button (click)=openLaunchWindow(app,id)>{{ app.name }}</button> + </div> + </div> --> + <app-strudelapplist [applist]=id.site.appCatalog [identity]=id></app-strudelapplist> + </mat-expansion-panel> + </div> + </mat-accordion> -<app-joblist></app-joblist> + </mat-sidenav> + <mat-sidenav-content> + <app-joblist [identity]=identity></app-joblist> + <div class="vspace"></div> + + </mat-sidenav-content> +</mat-sidenav-container> + +<!-- <app-joblist></app-joblist> --> diff --git a/src/app/launcher/launcher.component.ts b/src/app/launcher/launcher.component.ts index cb7a825c264ec5a3a15b45c134f9d354f8e511f0..de59d5705b684744a08922a61619755118bbaa71 100644 --- a/src/app/launcher/launcher.component.ts +++ b/src/app/launcher/launcher.component.ts @@ -1,12 +1,23 @@ -import { Component, OnInit, NgModule, Inject } from '@angular/core'; +import { Component, OnInit, NgModule, Inject, Input } from '@angular/core'; +import { MatDialog, MatDialogRef, MAT_DIALOG_DATA, MatDialogModule, MatDialogConfig } from '@angular/material'; +import { Location } from '@angular/common'; +// import { HttpClientModule, HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; +import { Observable } from 'rxjs/Observable'; +import { fromEvent } from 'rxjs'; +import { timer } from 'rxjs/observable/timer'; +import { repeat } from 'rxjs/operators'; + import {Strudelapp} from '../strudelapp'; import { StrudelappsService } from '../strudelapps.service'; -import { MatDialog, MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material'; import { TesService } from '../tes.service'; +import { AuthorisationService } from '../authorisation.service'; import { Identity } from '../identity'; import { Computesite } from '../computesite'; import { LogoutdialogComponent } from '../logoutdialog/logoutdialog.component'; -import { LogindialogComponent } from '../logindialog/logindialog.component' +// import { LogindialogComponent } from '../logindialog/logindialog.component'; +// import { LaunchdialogComponent } from '../launchdialog/launchdialog.component'; +import { BatchInterface} from '../batchinterface'; +import { SshAuthzServer } from '../identity'; @@ -17,36 +28,60 @@ import { LogindialogComponent } from '../logindialog/logindialog.component' }) export class LauncherComponent implements OnInit { public strudelapps: Strudelapp[]; + @Input() menuopen: boolean; public app: Strudelapp; public authorised: boolean; public identity: Identity; public identities: Identity[]; - - constructor( public dialog: MatDialog, private tesService: TesService) { + public sshauthzservers: SshAuthzServer[]; + private launchwindow: any; + private launchwindowWatcher: any; + + constructor( public dialog: MatDialog, + private tesService: TesService, + private authService: AuthorisationService) { + authService.SshAuthzServers.subscribe(o => {this.sshauthzservers = o}); + this.launchwindowWatcher = null; } ngOnInit() { - console.log('initialising launcher compoenent'); this.strudelapps = []; - setTimeout( () => { this.tesService.identities.subscribe(identities => this.updateIdentities(identities)); }); - console.log('initialising launcher compoenent complete'); + this.identities = []; + this.tesService.identities.subscribe(identities => this.updateIdentities(identities)); + // setTimeout( () => { this.tesService.identities.subscribe(identities => this.updateIdentities(identities)); }); + setTimeout( () => this.tesService.getIdentities() ) + } + compareFn = (o1, o2) => { + // Funny "=>" function syntax gives me access to "this" as LauncherComponent + // otherwise "this" would be set to the mat-select object. + if (o2 === undefined) { + + if (o1 === this.identity) { + return true; + } + } + if (o1 === o2) { + return true; + } + return false; } updateIdentities(identities) { - console.log('updateIdentities in launcher'); this.identities = identities; - console.log('updateIdentities in launcher complete'); - + console.log(this.identities); + if (this.identities.length > 0) { + this.identity = this.identities[0]; + // this.identity.site.appCatalog.subscribe(resp => this.updateApps()); + // this.updateApps(); + } else { + this.strudelapps = []; + } } -login () { - let dialogRef = this.dialog.open(LogindialogComponent, { - width: '250px', - height: '400px', - }); - return; +login (sshauthzserver) { + this.authService.login(sshauthzserver); } logout() { @@ -54,60 +89,28 @@ logout() { width: '250px', height: '400px', }); - // this.tesService.logout(); - // return; } + private buildParams(app: Strudelapp, identity: Identity, redirect): string { + let params = new URLSearchParams(); + let id = identity.copy_skip_catalog(); + id.site.appCatalog = null; + params.set('app',JSON.stringify(app)); + params.set('identity',JSON.stringify(id)); + params.set('redirect',redirect); + return params.toString(); + } - selectId(event: any) { - console.log('in selectID'); - if (!(event.value === undefined) && event.value instanceof(Identity)) { - this.identity=event.value; - this.strudelapps = this.identity.site.appCatalog; - } - console.log('selectID complete'); + + selectId(id: Identity) { + this.identity=id; } + configureResources() { let configwindow = window.open(this.identity.site.url+'configure/'+this.app.name); } - submitApp() { - this.tesService.getconfig(this.app, this.identity) - .subscribe(resp => {console.log(resp); this.tesService.submit(this.app,this.identity,resp)}); - } - - - - // openDialog(msg: string ): void { - // let dialogRef = this.dialog.open(LoginDialog, { - // width: '250px', - // height: '400px', - // data: { 'msg': msg } - // }); - // - // dialogRef.afterClosed().subscribe(result => { - // console.log('The dialog was closed'); - // }); - // } - - } - -// @Component({ -// selector: 'dialog-placeholder', -// templateUrl: 'dialog-placeholder.html', -// }) -// export class LoginDialog { -// -// constructor( -// public dialogRef: MatDialogRef<LoginDialog>, -// @Inject(MAT_DIALOG_DATA) public data: any) { } -// -// onNoClick(): void { -// this.dialogRef.close(); -// } -// -// } diff --git a/src/app/logindialog/logindialog.component.css b/src/app/logindialog/logindialog.component.css deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/app/logindialog/logindialog.component.html b/src/app/logindialog/logindialog.component.html deleted file mode 100644 index e83844b46f0259e81a61c6f3050447f9135e9093..0000000000000000000000000000000000000000 --- a/src/app/logindialog/logindialog.component.html +++ /dev/null @@ -1,3 +0,0 @@ - - <button mat-button (click)="onLogin()">Login to M3</button> - <button mat-button (click)="onCancel()">Cancel</button> diff --git a/src/app/logindialog/logindialog.component.spec.ts b/src/app/logindialog/logindialog.component.spec.ts deleted file mode 100644 index 39047f400a5544578aec85f0fd27ce4572fa6694..0000000000000000000000000000000000000000 --- a/src/app/logindialog/logindialog.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { LogindialogComponent } from './logindialog.component'; - -describe('LogindialogComponent', () => { - let component: LogindialogComponent; - let fixture: ComponentFixture<LogindialogComponent>; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ LogindialogComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(LogindialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/logindialog/logindialog.component.ts b/src/app/logindialog/logindialog.component.ts deleted file mode 100644 index 28bf80fa0eda1a5094efe40e0e003d881f0ff789..0000000000000000000000000000000000000000 --- a/src/app/logindialog/logindialog.component.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Component, OnInit, Inject } from '@angular/core'; -import { MatDialog, MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material'; -import { TesService } from '../tes.service'; -import {AuthorisationService} from '../authorisation.service'; - - - -@Component({ - selector: 'app-logindialog', - templateUrl: './logindialog.component.html', - styleUrls: ['./logindialog.component.css'] -}) -export class LogindialogComponent implements OnInit { - - constructor( - public dialogRef: MatDialogRef<LogindialogComponent>, - @Inject(MAT_DIALOG_DATA) public data: any, - private tesService: TesService, - private authService: AuthorisationService ) { - } - - ngOnInit() { - } - - onLogin() { - this.authService.login(); - } - onCancel() { - this.dialogRef.close(); - } - - -} diff --git a/src/app/mock-compute-site.ts b/src/app/mock-compute-site.ts deleted file mode 100644 index 26e6050286447c57728bd45d358956180aec0754..0000000000000000000000000000000000000000 --- a/src/app/mock-compute-site.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Computesite } from './computesite'; -import { STRUDELAPPS } from './mock-strudel-app'; - -export const COMPUTESITES: Computesite[] = [ - { url: 'https://vm-118-138-240-255.erc.monash.edu.au/m3siteconfig/', name: 'M3', - host: 'm3.massive.org.au', - cafingerprint: 'RSA SHA256:cmDxHrZQSPlBMUUcI/BWmruXho1XOzfXPDHSqVTwV2I', - appCatalog: STRUDELAPPS } -]; diff --git a/src/app/mock-strudel-app.ts b/src/app/mock-strudel-app.ts deleted file mode 100644 index 3feadbf0c0fbb99017b424f16a797822f672aa69..0000000000000000000000000000000000000000 --- a/src/app/mock-strudel-app.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Strudelapp } from './computesite'; - -const tmuxscript = `#!/bin/bash -#SBATCH -J tmux -tmux new-session -d -s $SLURM_JOB_NAME bash -# determine the process id of the tmux server -pid=$( /bin/ps x | /bin/grep -i "[t]mux new-session -d -s" | sed 's/^\ *//' | cut -f 1 -d " " ) -ps x -# Sleep until the tmux server exits -while [ -e /proc/$pid ]; do sleep 5; done -` - -const jupyterscript = `#!/bin/bash -#SBATCH -J Jupyter -/projects/pMOSP/chines/jupyter-venv/bin/jupyter-notebook -` - -const stddesktop = `#!/bin/bash -#SBATCH --job-name=desktop -# --exclusive allows the job to consume all resources on the node reguardless of how many cpus/gpus/memory/etc -#SBATCH --exclusive -#SBATCH --nodes=1 -#SBATCH --ntasks=3 -#SBATCH --cpus-per-task=1 -#SBATCH --gres=gpu:K1:1 -#SBATCH --partition=m3f -#SBATCH --mem=10288M - -#module purge -module load tigervnc/1.8.0 -#module list -export PATH=$PATH:$XMASSIVESERVICEPATH/bin -export XDG_CONFIG_DIRS=$XMASSIVESERVICEPATH/xdg_config -export XDG_DATA_DIRS=$XMASSIVESERVICEPATH/xdg_data:/usr/share:/usr/local/share -echo " Starting VNC..." -#Remove old pid files that should not exist due to vncserver not being shut down cleanly last time -#rm -f ~/.vnc/$HOSTNAME:1.pid -#vncserver -xstartup $XSTARTUP #:1 so we can start multiple VNC servers -vncserver -xstartup /usr/local/desktop/services/massive-std/xstartup/xstartup -xinit /usr/bin/xterm -- -sharevts -novtswitch -config xorg.conf & -#vncserver -while true; -do - sleep 30; -done` - -const vncviewer = { 'cmd': ['python','vncviewer.py','--password','{password}','--host','localhost','--port','{localtunnelport}'], 'redir':'?password={password}'} - -const tmuxclient = {'cmd': ['/usr/bin/gnome-terminal', '--','ssh','-t', - 'localhost','-p','{localtunnelport}','-o', - 'StrictHostkeyChecking=No','/bin/tmux a {sessionname}'], - 'redir': null} - - -const jupyterclient = {'cmd': null, 'redir': '?token={token}'} - -export const STRUDELAPPS: Strudelapp[] = [ - // { url: null, name: 'tmux', startscript: tmuxscript, - // paramscmd: '/home/chines/smuxparams.py', client: tmuxclient, localbind: false }, - { url: null, name: 'Jupyter Notebook', startscript: jupyterscript, - paramscmd: '/home/chines/jupyter_params.py', client: jupyterclient, localbind: true }, - // { url: null, name: 'Standard Desktop', startscript: stddesktop, - // paramscmd: '/home/chines/desktop_params.py', client: vncviewer, localbind: false }, -]; diff --git a/src/app/siteselection/siteselection.component.css b/src/app/siteselection/siteselection.component.css deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/app/siteselection/siteselection.component.html b/src/app/siteselection/siteselection.component.html deleted file mode 100644 index 06dbdec83763708076be078b2e4440cb4d223ab8..0000000000000000000000000000000000000000 --- a/src/app/siteselection/siteselection.component.html +++ /dev/null @@ -1,13 +0,0 @@ - - <div> - <mat-form-field> - <mat-select placeholder="Site" [(value)]=siteurl> - <mat-option *ngFor="let site of computesites" [value]=site.url> - {{site.name}} - </mat-option> - </mat-select> - </mat-form-field> - <div *ngIf="siteurl"> - <!--<button mat-button (click)=configureResources()>Configure Resources</button>--> - </div> -</div> diff --git a/src/app/siteselection/siteselection.component.spec.ts b/src/app/siteselection/siteselection.component.spec.ts deleted file mode 100644 index d77e66690e21160c5610676356ae43a9f6cf1cff..0000000000000000000000000000000000000000 --- a/src/app/siteselection/siteselection.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { SiteselectionComponent } from './siteselection.component'; - -describe('SiteselectionComponent', () => { - let component: SiteselectionComponent; - let fixture: ComponentFixture<SiteselectionComponent>; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ SiteselectionComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(SiteselectionComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/siteselection/siteselection.component.ts b/src/app/siteselection/siteselection.component.ts deleted file mode 100644 index 0f4408b546fc2007fed5c5e451fd2fe86901eb31..0000000000000000000000000000000000000000 --- a/src/app/siteselection/siteselection.component.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { Computesite } from '../computesite' -import { ComputesitesService} from '../computesites.service'; - -@Component({ - selector: 'app-siteselection', - templateUrl: './siteselection.component.html', - styleUrls: ['./siteselection.component.css'] -}) -export class SiteselectionComponent implements OnInit { - public computesites: Computesite[]; - public siteurl: String; - constructor(private computesiteSerivce: ComputesitesService) { } - - ngOnInit() { - this.loadComputesites(); - } - configureResources() { - console.log('configuring resources for',this.siteurl); - } - siteselect(a: any) { - console.log('siteselect') - } - - loadComputesites() { - this.computesites = this.computesiteSerivce.getComputeSites() - } - -} diff --git a/src/app/strudelapp.ts b/src/app/strudelapp.ts index 8cc0a737e87896a44f84457ec52993f2d4e38235..59b85b084e3200891ba048417af0f70d96f1f57f 100644 --- a/src/app/strudelapp.ts +++ b/src/app/strudelapp.ts @@ -9,4 +9,5 @@ export class Strudelapp { localbind: boolean; // does the application bind to a port on the localhost // interface or on all interfaces. This behaviour determins /// how we create tunnels + applist: Strudelapp[] = null; } diff --git a/src/app/strudelapps.service.ts b/src/app/strudelapps.service.ts index 30febe9ce486612064be440ac2aba39bdef66393..e6ed43f4390d6924846f3dd29627dbe208b6a272 100644 --- a/src/app/strudelapps.service.ts +++ b/src/app/strudelapps.service.ts @@ -1,6 +1,5 @@ import { Injectable } from '@angular/core'; import { Strudelapp } from './strudelapp'; -import { STRUDELAPPS } from './mock-strudel-app'; @Injectable({ providedIn: 'root', @@ -8,12 +7,29 @@ import { STRUDELAPPS } from './mock-strudel-app'; export class StrudelappsService { constructor() { } - getStrudelapps(): Strudelapp[] { - return STRUDELAPPS; - } - getApp(name: string) { - return STRUDELAPPS[0]; + // getApp(name: string,applist: Strudelapp[]): Strudelapp { + getApp(name: string,applist: any): any { + + var idx: number; + var app: any; + var sapp: Strudelapp; + app = applist[0]; + for ( let item of applist) { + sapp = <Strudelapp>item; + idx = sapp.name.toLowerCase().indexOf(name.toLowerCase()) + if (idx == 0) { + app = item; + return app; + } + if (sapp.applist != null) { + app = this.getApp(name,sapp.applist); + if (app != null) { + return app; + } + } + } + return null; } } diff --git a/src/app/tes.service.ts b/src/app/tes.service.ts index fd9b487261dcaa965e122480534f5889724b5e7f..2c661057e70dadc5f6d56a44788d4886d441181f 100644 --- a/src/app/tes.service.ts +++ b/src/app/tes.service.ts @@ -1,55 +1,86 @@ import { Injectable } from '@angular/core'; import { HttpClientModule, HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; +import { fromEvent, throwError } from 'rxjs'; import { of } from 'rxjs/observable/of'; import { catchError, map, tap } from 'rxjs/operators'; import { Job } from './job'; import {BehaviorSubject} from 'rxjs/BehaviorSubject'; -import {Subject} from 'rxjs/Subject'; +import { Subject } from 'rxjs/Subject'; import { Strudelapp } from './strudelapp'; import { Computesite } from './computesite'; -import { Identity, AuthToken, KeyCert, AuthService } from './identity'; +import { Identity, AuthToken, KeyCert, SshAuthzServer } from './identity'; import { BatchInterface} from './batchinterface'; import {ComputesitesService} from './computesites.service'; import { StrudelappsService } from './strudelapps.service'; import { timer } from 'rxjs/observable/timer'; import { repeat } from 'rxjs/operators'; -import {LocationStrategy} from '@angular/common'; +import {LocationStrategy, Location} from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; +import { ModaldialogComponent } from './modaldialog/modaldialog.component'; +import { MatDialog, MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material'; +import { MatSnackBar } from '@angular/material'; + + + @Injectable({ providedIn: 'root', }) export class TesService { -public Base='https://vm-118-138-240-255.erc.monash.edu.au/tes' +// public Base='https://vm-118-138-240-255.erc.monash.edu.au/tes' +public Base='http://localhost:5000'; public statusMsg: BehaviorSubject<any>; public jobs: any[]; public busy: BehaviorSubject<boolean> ; public joblist: BehaviorSubject<{ [id: string ]: Job[]}>; private timerSubscription: any; -public keyCert: Subject<KeyCert>; +// public keyCert: Subject<KeyCert>; public identities: BehaviorSubject<Identity[]>; -private batchinterface: {[id: string] : BatchInterface}; +private appwindow: any; +private appwindowWatcher: any; +public batchinterface: {[id: string] : BatchInterface}; +private snackBarRef: any; constructor(private http: HttpClient, + public dialog: MatDialog, private computesite: ComputesitesService, - private strudelappsService: StrudelappsService) { + private strudelappsService: StrudelappsService, + public snackBar: MatSnackBar, + private location: Location ) { this.statusMsg = new BehaviorSubject<any>(''); + this.statusMsg.subscribe(msg => this.displayMessage(msg)); this.busy = new BehaviorSubject<boolean>(false); this.joblist = new BehaviorSubject<{[id: string]: Job[]}>({}); this.timerSubscription = null; - this.keyCert = new Subject<KeyCert>(); + this.appwindowWatcher = null; + // this.keyCert = new Subject<KeyCert>(); this.identities= new BehaviorSubject<Identity[]>([]); - this.keyCert.subscribe(keyCert => this.sshAdd(keyCert)); + // this.keyCert.subscribe(keyCert => this.sshAdd(keyCert)); this.batchinterface = {}; - this.getIdentities(); + // this.getIdentities(); + } + + private displayMessage(msg) { + if (msg === null || msg == "") { + console.log('attempting to dismis snackbar'); + if (this.snackBarRef != undefined) { + this.snackBarRef.dismiss() + } + } else { + this.snackBarRef = this.snackBar.open(msg,'Dismiss'); + } } private buildParams(app: Strudelapp, identity: Identity, batchinterface: BatchInterface): string { let params = new URLSearchParams(); + let params2 = new URLSearchParams(); + let id = identity.copy_skip_catalog(); + id.site.appCatalog = null; + params.set('app',JSON.stringify(app)); params.set('interface',JSON.stringify(batchinterface)); - params.set('identity',JSON.stringify(identity)); + params.set('identity',JSON.stringify(id)); return params.toString(); } @@ -58,18 +89,16 @@ private batchinterface: {[id: string] : BatchInterface}; let alljobs = this.joblist.value; let i = 0; for (let j of joblist) { - j.app = this.strudelappsService.getApp(j.name); + j.app = this.strudelappsService.getApp(j.name,identity.site.appCatalog.value); j.identity = identity; } alljobs[identity.repr()] = joblist; this.joblist.next(alljobs); - this.statusMsg.next(null); } getJobs() { let headers = new HttpHeaders(); let options = { headers: headers, withCredentials: true}; - this.statusMsg.next(null); // remove from the job list any jobs for identities that we don't know about let oldjobs = this.joblist.value; let oldjobkeys = Object.keys(oldjobs); @@ -87,18 +116,20 @@ private batchinterface: {[id: string] : BatchInterface}; for (let identity of this.identities.value) { if (this.batchinterface[identity.repr()] === undefined) { this.getconfig(new Strudelapp(),identity) - .subscribe(resp => this.batchinterface[identity.repr()] = resp); + .pipe(catchError(this.handleError)) + .subscribe(resp => this.batchinterface[identity.repr()] = resp, + error => this.httperror(error)); continue; } let paramstr = this.buildParams(null,identity,this.batchinterface[identity.repr()]); this.http.get<Job[]>(this.Base+'/stat'+'?'+paramstr,options) - .pipe(catchError(this.handleError('getJobs',[]))) - .subscribe(resp => this.updateJoblist(resp,identity)); + .pipe(catchError(this.handleError)) + .subscribe(resp => this.updateJoblist(resp,identity), + error => this.httperror(error)); } } private startPolling() { - this.statusMsg.next(null); if (!(this.timerSubscription === null)) { this.timerSubscription.unsubscribe() } @@ -116,9 +147,15 @@ private batchinterface: {[id: string] : BatchInterface}; let headers = new HttpHeaders(); let options = { headers: headers, withCredentials: true}; return this.http.get<any>(identity.site.url+'getconfig/'+app.name,options) - .pipe(catchError(this.handleError('getconfig',[]))) + .pipe(catchError(this.handleError)) } + submissionError(message: string) { + this.statusMsg.next(message); + this.busy.next(false); + console.log(message); + } + submit(app: Strudelapp, identity: Identity, batchinterface: BatchInterface) { let headers = new HttpHeaders(); let options = { headers: headers, withCredentials: true}; @@ -126,43 +163,65 @@ private batchinterface: {[id: string] : BatchInterface}; this.busy.next(true); let paramstr = this.buildParams(app,identity,batchinterface); this.http.post<any>(this.Base+'/submit'+'?'+paramstr,{}, options) - .pipe(catchError(this.handleError('submit',[]))) - .subscribe(resp => this.busy.next(false)); + .pipe(catchError(this.handleError)) + .subscribe(resp => { + this.busy.next(false); + this.statusMsg.next(null) + }, + error => this.submissionError(error)); } submitted(resp: any ) { this.busy.next(false); - this.statusMsg.next('Updating job list'); + // this.statusMsg.next(null); this.getJobs(); } cancel(job: Job) { - console.log("In tes cancel"); let headers = new HttpHeaders(); let options = { headers: headers, withCredentials: true}; - this.statusMsg.next(null); + // this.statusMsg.next(null); let data = {}; - console.log(job.identity); - console.log(job.identity.repr); let paramstr = this.buildParams(job.app,job.identity,this.batchinterface[job.identity.repr()]); this.http.delete<any>(this.Base+'/cancel/'+job.jobid+'?'+paramstr, options) - .pipe(catchError(this.handleError('cancel',[]))) + .pipe(catchError(this.handleError)) .subscribe(resp => this.submitted(resp)); - console.log('tes.cancel complete'); + } + + public watchAppwindow(appwindow, dialogRef) { + if (appwindow.closed) { + dialogRef.close(); + } } public connect(job: Job) { - this.statusMsg.next(null); + // this.statusMsg.next(null); let headers = new HttpHeaders(); let options = { headers: headers, withCredentials: true}; this.busy.next(true); let paramstr = this.buildParams(job.app,job.identity,this.batchinterface[job.identity.repr()]); - let appwindow = window.open(this.Base+"/connect/"+job.jobid+"/"+job.batch_host+"?"+paramstr); - appwindow.focus(); + // let appwindow = window.open(this.Base+"/connect/"+job.jobid+"/"+job.batch_host+"?"+paramstr); + + // let windowloc = this.Base+"/connect/"+job.jobid+"/"+job.batch_host+"?"+paramstr; + let url = this.Base+"/connect/"+job.jobid+"/"+job.batch_host; + let windowloc = './connecting?connecturl='+url+"&"+paramstr; + // let windowloc = this.router.config + this.appwindow = window.open(windowloc); + if (!(this.appwindowWatcher === null)) { + this.appwindowWatcher.unsubscribe() + } + + let dialogRef = this.dialog.open(ModaldialogComponent, { + width: '600px', + }); + this.appwindowWatcher = timer(500).pipe(repeat()).subscribe(() => this.watchAppwindow(this.appwindow,dialogRef)); + // this.appwindow.location.assign(windowloc); + this.appwindow.focus(); this.busy.next(false); } -private sshAdd(keyCert: KeyCert) { + +public sshAdd(keyCert: KeyCert) { if (keyCert.key == undefined) { return; } @@ -172,19 +231,19 @@ private sshAdd(keyCert: KeyCert) { this.statusMsg.next("Authorising ...") let data = {'key': keyCert.key, 'cert': keyCert.cert}; this.http.post<any>(this.Base+'/sshagent',data,options) - .pipe(catchError(this.handleError('storeCert',[]))) + .pipe(catchError(this.handleError)) .subscribe(resp => this.getIdentities(), - error => this.httperror(error)); + error => this.httperror(error)) } -private getIdentities() { - console.log('retrieving current identities'); - this.statusMsg.next("Updating the list of available accounts") +public getIdentities() { + this.statusMsg.next("Updating the list of available accounts"); let headers = new HttpHeaders(); let options = { headers: headers, withCredentials: true}; this.http.get<any>(this.Base+'/sshagent',options) - .pipe(catchError(this.handleError('getIdentities',[]))) - .subscribe(resp => this.updateIdentities(resp)); + .pipe(catchError(this.handleError)) + .subscribe(resp => this.updateIdentities(resp), + error => this.httperror(error)) } private killAgent() { @@ -192,30 +251,39 @@ private killAgent() { let headers = new HttpHeaders(); let options = { headers: headers, withCredentials: true}; this.http.delete<any>(this.Base+'/sshagent',options) - .pipe(catchError(this.handleError('killAgent',[]))) - .subscribe(resp => this.updateIdentities(resp)); + .pipe(catchError(this.handleError)) + .subscribe(resp => this.updateIdentities(resp), + error => this.httperror(error)); } private updateIdentities(resp) { - //TODO Each cert as the signing CA parameter. Use this to find the compute sites - // rather than just assuming sites[0] let certcontents = resp; let identities = []; - let sites = this.computesite.getComputeSites(); + var cs: any; let idsShort = [] - for (let i in certcontents) { - identities.push(new Identity(certcontents[i].Principals[0],sites[0])) + for (cs of this.computesite.returnComputeSites()) { + let computesite = <Computesite>cs; + for (let i in certcontents) { + var fp: string = (<Computesite>cs).cafingerprint; + for (let ca of certcontents[i]['Signing CA']) { + if (ca == fp) { + identities.push(new Identity(certcontents[i].Principals[0],<Computesite>cs)) + } + } + } } this.identities.next(identities); + this.statusMsg.next(null); if (identities.length == 0) { - this.statusMsg.next(null); return; } this.startPolling(); } public logout(): Boolean { - this.statusMsg.next(null); + for (let id of this.identities.value) { + console.log('logout identitiy',id.authservice); + } this.killAgent(); return true; } @@ -224,25 +292,40 @@ public getstatusMsgSubject(): BehaviorSubject<any> { return this.statusMsg; } -private httperror(error: any) { - console.log(error); +private httperror(errorstr: string) { + var re = /login expired/gi; + let searchresult = errorstr.search(re); + if (searchresult != -1) { + this.statusMsg.next("Some authentication tokens have expired, you may need to log in again"); + this.getIdentities(); + } + console.log(errorstr); } -private handleError<T> (operation = 'operation', result?: T) { - return (error: any): Observable<T> => { - console.log('in handle error',operation); - console.log(error.status); - - if ( operation == 'getJobs') { - this.statusMsg.next("Hmm, that didn't work. If you're using a local connection, please make sure Strudel-TES is running."); - } else if (operation == 'submit') { - this.statusMsg.next("Hmm, I couldn't submit that job") - } else if (operation == 'getIdentities') { - this.statusMsg.next("Hmm, I coudln't get any data from the backend. Try refreshing this page") - } else { - console.error(error); +private handleError(error: HttpErrorResponse) { + if (error.error instanceof ErrorEvent) { + console.log('instance of ErrorEvent'); + console.log(error); + // A client-side or network error occurred. Handle it accordingly. + return throwError("Hmm, that didn't work. If you're using a local connection, please make sure Strudel-TES is running."); + } else { + console.log('NOT an instance of ErrorEvent'); + console.log(error); + // Not sure if this code works correctly. It should update identities in case the error is + // that the user isn't allowed to run the job + + var re = /identity/gi; + let searchresult = error.error.message.search(re); + if (searchresult != -1) { + // this.getIdentities(); + return throwError('login expired, refreshing'); } - return of(result as T); - }; + return throwError(error.error.message) + // this.statusMsg.next("There was an error submitting that job. The backend gave me the message: " + error.error.message); + + } } + + + } diff --git a/src/app/teserrors/teserrors.component.html b/src/app/teserrors/teserrors.component.html index d8a160b2f58aa03e7b8ec2c46d9307eaa441d255..78998789efa824997dd15d89d8fd28738f22a497 100644 --- a/src/app/teserrors/teserrors.component.html +++ b/src/app/teserrors/teserrors.component.html @@ -1,3 +1,3 @@ -<p> +<!-- <p> {{ statusMsg }} -</p> +</p> --> diff --git a/src/app/teserrors/teserrors.component.ts b/src/app/teserrors/teserrors.component.ts index 607b7d1c2e728cf7f4d1319de487611eddcc060f..386fc599033ed3ef640e838dda4297df2819f1fb 100644 --- a/src/app/teserrors/teserrors.component.ts +++ b/src/app/teserrors/teserrors.component.ts @@ -1,5 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { TesService } from '../tes.service'; +import { MatSnackBar } from '@angular/material'; + @Component({ selector: 'app-teserrors', @@ -8,22 +10,22 @@ import { TesService } from '../tes.service'; }) export class TeserrorsComponent implements OnInit { - public statusMsg: string; - - constructor(private tesService: TesService,) { } - + // public statusMsg: string; + private snackBarRef: any; + constructor(private tesService: TesService, public snackBar: MatSnackBar,) { } ngOnInit() { - console.log('constructing message component'); - this.tesService.getstatusMsgSubject().subscribe( msg => this.getNewMessage(msg)); - console.log('constructing message component complete'); - + // this.tesService.getstatusMsgSubject().subscribe( msg => this.getNewMessage(msg)); } getNewMessage(msg) { - console.log('teserrors get message'); - this.statusMsg = msg; - console.log('teserrors get message complete'); - + if (msg === null) { + console.log('attempting to dismis snackbar'); + if (this.snackBarRef != undefined) { + this.snackBarRef.dismiss() + } + } else { + this.snackBarRef = this.snackBar.open(msg,'Dismiss'); + } } } diff --git a/src/styles.css b/src/styles.css index 5f69d4f7f3fd9c31aaf694a3b57fb4007c22918c..f81c956193d096ce740bbff4ecaee6307770d88a 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,2 +1,3 @@ /* You can add global styles to this file, and also import other style files */ +@import url( 'https://fonts.googleapis.com/css?family=Roboto:400,700|Material+Icons'); @import "~@angular/material/prebuilt-themes/indigo-pink.css";