From d1d506ef8bb038bfe066bdc18bd29103332e0eee Mon Sep 17 00:00:00 2001
From: Chris Hines <chris.hines@monash.edu>
Date: Mon, 23 Jul 2018 09:41:53 +1000
Subject: [PATCH] lots of development to sv2

---
 src/app/app.component.html                    |   4 +-
 src/app/app.component.ts                      |   1 -
 src/app/app.module.ts                         |  11 +-
 src/app/computesite.ts                        |  14 +
 src/app/computesites.service.ts               |   4 +-
 src/app/identity.ts                           |  35 ++-
 src/app/job/job.component.ts                  |   5 +
 src/app/joblist/joblist.component.html        |  18 +-
 src/app/joblist/joblist.component.ts          |  34 ++-
 src/app/launcher/launcher.component.html      |  25 +-
 src/app/launcher/launcher.component.ts        | 118 +++++---
 src/app/logindialog/logindialog.component.css |   0
 .../logindialog/logindialog.component.html    |   3 +
 .../logindialog/logindialog.component.spec.ts |  25 ++
 src/app/logindialog/logindialog.component.ts  |  31 ++
 .../logoutdialog/logoutdialog.component.css   |   0
 .../logoutdialog/logoutdialog.component.html  |   3 +
 .../logoutdialog.component.spec.ts            |  25 ++
 .../logoutdialog/logoutdialog.component.ts    |  30 ++
 src/app/mock-compute-site.ts                  |   6 +-
 src/app/mock-strudel-app.ts                   |  10 +-
 src/app/strudelapps.service.ts                |  13 +-
 src/app/tes.service.ts                        | 273 +++++++++++-------
 src/app/teserrors/teserrors.component.ts      |   6 +
 24 files changed, 496 insertions(+), 198 deletions(-)
 create mode 100644 src/app/logindialog/logindialog.component.css
 create mode 100644 src/app/logindialog/logindialog.component.html
 create mode 100644 src/app/logindialog/logindialog.component.spec.ts
 create mode 100644 src/app/logindialog/logindialog.component.ts
 create mode 100644 src/app/logoutdialog/logoutdialog.component.css
 create mode 100644 src/app/logoutdialog/logoutdialog.component.html
 create mode 100644 src/app/logoutdialog/logoutdialog.component.spec.ts
 create mode 100644 src/app/logoutdialog/logoutdialog.component.ts

diff --git a/src/app/app.component.html b/src/app/app.component.html
index 10e69ef..19425a3 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -4,8 +4,8 @@
     <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>
+      <!-- <button  mat-button *ngIf="authorised" (click)="logout()">Logout</button>
+      <button  mat-button *ngIf="!authorised" (click)="login()">Login</button> -->
     </mat-toolbar-row>
   </mat-toolbar>
 
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 963b7ef..8c6a42b 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -17,7 +17,6 @@ export class AppComponent {
    };
 
   ngOnInit() {
-    setTimeout(() =>  { this.tesService.authorised.subscribe(authorised => { this.authorised = authorised }); });
     // this.tesService.testingAuth.subscribe(testingAuth => { this.testingAuth = testingAuth; console.log('testingAuth updated'+this.testingAuth) });
     // this.testingAuth = false;
   }
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index e8ba57e..51a81f2 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -3,7 +3,7 @@ import { NgModule } from '@angular/core';
 
 
 import { AppComponent } from './app.component';
-import { LauncherComponent, DialogPlaceholder } from './launcher/launcher.component';
+import { LauncherComponent } from './launcher/launcher.component';
 import { JoblistComponent } from './joblist/joblist.component';
 import { MatButtonModule } from '@angular/material';
 import { MatFormFieldModule } from '@angular/material';
@@ -27,6 +27,8 @@ 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 { LogoutdialogComponent } from './logoutdialog/logoutdialog.component';
 
 
 
@@ -38,8 +40,9 @@ import { TokenextractorComponent } from './tokenextractor/tokenextractor.compone
     JobComponent,
     SiteselectionComponent,
     TeserrorsComponent,
-    DialogPlaceholder,
-    TokenextractorComponent
+    TokenextractorComponent,
+    LogindialogComponent,
+    LogoutdialogComponent
   ],
   imports: [
   BrowserModule,
@@ -60,7 +63,7 @@ import { TokenextractorComponent } from './tokenextractor/tokenextractor.compone
   AppRoutingModule,
 
   ],
-  entryComponents: [ DialogPlaceholder ],
+  entryComponents: [ LogindialogComponent, LogoutdialogComponent ],
   providers: [ StrudelappsService, ComputesitesService, TesService, MatDialog],
   bootstrap: [AppComponent]
 })
diff --git a/src/app/computesite.ts b/src/app/computesite.ts
index 567f55a..a6de7f3 100644
--- a/src/app/computesite.ts
+++ b/src/app/computesite.ts
@@ -7,4 +7,18 @@ 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[];
+}
+
+export class Strudelapp {
+  url: string; // A url used to retrieve extra config options. May be null
+  name: string; // Human readable name
+  startscript: string; // batch script ... should NOT include resource directives
+                       // resource directives like #SBATCH belong in the batchinterface
+  paramscmd: string;  // command to return extra data such as passwords and tokens
+                      // values returned here can be used in the client strings
+  client: {cmd: string[], redir: string};
+  localbind: boolean; // does the application bind to a port on the localhost
+                      // interface or on all interfaces. This behaviour determins
+                      /// how we create tunnels
 }
diff --git a/src/app/computesites.service.ts b/src/app/computesites.service.ts
index 0a92e89..219535a 100644
--- a/src/app/computesites.service.ts
+++ b/src/app/computesites.service.ts
@@ -2,7 +2,9 @@ import { Injectable } from '@angular/core';
 import { Computesite } from './computesite';
 import { COMPUTESITES } from './mock-compute-site';
 
-@Injectable()
+@Injectable({
+  providedIn: 'root',
+})
 export class ComputesitesService {
 
   constructor() { }
diff --git a/src/app/identity.ts b/src/app/identity.ts
index 94fd05a..952370b 100644
--- a/src/app/identity.ts
+++ b/src/app/identity.ts
@@ -1,7 +1,40 @@
 import {Computesite} from './computesite';
-// Identities are defined by a username on a computer, but rather than just 
+// Identities are defined by a username on a computer, but rather than just
 // DNS entry, there is extra info in the Computesite
 export class Identity {
   username: string;
   site: Computesite;
+  authservice: AuthService;
+  constructor( username: string, site: Computesite) {
+    this.username = username;
+    this.site = site;
+  }
+  displayName(): string {
+    return this.username+'@'+this.site.name;
+  }
+  repr(): string {
+    return JSON.stringify([this.username,this.site.cafingerprint,this.site.host]);
+  }
+}
+
+export class AuthToken {
+  token: string;
+  authservice: AuthService;
+  constructor( token: string, authservice: AuthService ) {
+    this.token = token;
+    this.authservice = authservice;
+  }
+}
+
+export class KeyCert {
+  key: string;
+  cert: string;
+  authservice: AuthService;
+}
+
+export class AuthService {
+  base: string;
+  authorise: string;
+  sign: string;
+  client_id: string;
 }
diff --git a/src/app/job/job.component.ts b/src/app/job/job.component.ts
index cfcdbd3..c1518a7 100644
--- a/src/app/job/job.component.ts
+++ b/src/app/job/job.component.ts
@@ -19,15 +19,20 @@ export class JobComponent implements OnInit {
 }
 
   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');
+
   }
 
   onCancel() {
+    this.jobdata.app = this.strudelAppsService.getApp(this.jobdata.name);
+    console.log(this.jobdata);
     this.tesService.cancel(this.jobdata);
   }
 
diff --git a/src/app/joblist/joblist.component.html b/src/app/joblist/joblist.component.html
index 7f6ea40..0ba48dc 100644
--- a/src/app/joblist/joblist.component.html
+++ b/src/app/joblist/joblist.component.html
@@ -1,8 +1,16 @@
-<div *ngIf="jobs.length == 0">
+<div *ngIf="identities.length == 0">
   <mat-card>
-  You don't appear to have any jobs
-</mat-card>
+    Click identity and login to a service to start an application.
+  </mat-card>
 </div>
-<div *ngFor="let job of jobs">
-  <app-job [jobdata]=job></app-job>
+<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>
+  <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 21b0007..f27d908 100644
--- a/src/app/joblist/joblist.component.ts
+++ b/src/app/joblist/joblist.component.ts
@@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
 import {TesService} from '../tes.service';
 import { Job } from '../job';
 import { Observable } from 'rxjs/Observable';
+import { Identity } from '../identity';
 
 
 
@@ -11,16 +12,20 @@ import { Observable } from 'rxjs/Observable';
   styleUrls: ['./joblist.component.css']
 })
 export class JoblistComponent implements OnInit {
-  public jobs: any[] = [];
+  public jobs: {[id: string]: Job[] } = {};
+  public identities: Identity[];
   private displayedColumns = ['id'];
   private jobsSubscription: any;
+  private idSubscription: any;
 
-  constructor(private tesService: TesService,) { }
+  constructor(private tesService: TesService,) {
+}
 
   ngOnInit() {
-
-    this.jobsSubscription = this.tesService.joblist.subscribe(jobs => this.jobs = jobs);
-    console.log("jobs subscribed by joblist component");
+    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");
   }
 
   public ngOnDestroy(): void {
@@ -29,21 +34,18 @@ export class JoblistComponent implements OnInit {
       }
 
   }
+  updateIds(identities: Identity[]) {
+    console.log('update identities');
+    this.identities = identities;
+    console.log('update identities complete');
 
-  updateJobs(jobs) {
-    this.jobs = jobs;
-    console.log("job data received by the joblist component");
-    console.log(this.jobs);
   }
 
-  login(){
-    //Wire this up to the tes login component to get an ssh cert on the tes backend
-    //This will also tell the TES which site we are using
-    return;
+  updateJobs(jobs) {
+    console.log('updating jobs')
+    this.jobs = jobs;
+    console.log('update jobs complete')
   }
 
-  logout() {
-    return;
-  }
 
 }
diff --git a/src/app/launcher/launcher.component.html b/src/app/launcher/launcher.component.html
index 38c925b..5ad7440 100644
--- a/src/app/launcher/launcher.component.html
+++ b/src/app/launcher/launcher.component.html
@@ -1,5 +1,18 @@
 
-<div *ngIf="authorised">
+<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>
+
+	</mat-select>
+</mat-form-field>
 <mat-form-field>
 	<mat-select placeholder="Application" [(value)]=app>
 		<mat-option *ngFor="let a of strudelapps" [value]=a>
@@ -7,12 +20,8 @@
 		</mat-option>
 	</mat-select>
 </mat-form-field>
-<button mat-button (click)=configureResources()>Configure Resources</button>
-<button mat-button (click)=configureApp() [disabled]="!app">Configure App</button>
-<button mat-button (click)=submitApp() [disabled]="!app">Start</button>
+<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>
 
 <app-joblist></app-joblist>
-
-
-
-</div>
diff --git a/src/app/launcher/launcher.component.ts b/src/app/launcher/launcher.component.ts
index bbd4c4c..6c1db2c 100644
--- a/src/app/launcher/launcher.component.ts
+++ b/src/app/launcher/launcher.component.ts
@@ -3,7 +3,10 @@ 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 {Identity } from '../identity';
+import { Identity } from '../identity';
+import { Computesite } from '../computesite';
+import { LogoutdialogComponent } from '../logoutdialog/logoutdialog.component';
+import { LogindialogComponent } from '../logindialog/logindialog.component'
 
 
 
@@ -18,68 +21,93 @@ export class LauncherComponent implements OnInit {
   private app: Strudelapp;
   public authorised: boolean;
   private identity: Identity;
+  private identities: Identity[];
 
-  constructor(private strudelappsService: StrudelappsService, public dialog: MatDialog, private tesService: TesService) { }
-
-  ngOnInit() {
-    this.loadStrudelapps();
-    setTimeout( () => { this.tesService.authorised.subscribe(auth => this.authorised = auth); });
+  constructor( public dialog: MatDialog, private tesService: TesService) {
   }
 
+  ngOnInit() {
+    console.log('initialising launcher compoenent');
+    this.strudelapps = [];
+    setTimeout( () => { this.tesService.identities.subscribe(identities => this.updateIdentities(identities)); });
+    console.log('initialising launcher compoenent complete');
 
-  configureApp() {
-    console.log('configuring app for',this.app.name);
-    this.openDialog('If the application takes any parameters, this will open a new window where you can configure them')
   }
 
-  configureResources() {
-    this.openDialog('This will open a new window to configure resource reqests')
-  }
+  updateIdentities(identities) {
+    console.log('updateIdentities in launcher');
+    this.identities = identities;
+    console.log('updateIdentities in launcher complete');
 
-  submitApp() {
-    console.log('submitting app for',this.app.name);
-    this.identity = this.tesService.identities[0];
-    this.tesService.submit(this.app, this.identity);
-    // this.openDialog('This is where we should start the app');
   }
 
-  appselect(a: any) {
-    console.log('appselect')
-    this.app = a;
-  }
+login () {
+    let dialogRef = this.dialog.open(LogindialogComponent, {
+      width: '250px',
+      height: '400px',
+    });
+    return;
+}
+
+logout() {
+    let dialogRef = this.dialog.open(LogoutdialogComponent, {
+      width: '250px',
+      height: '400px',
+    });
+    // this.tesService.logout();
+    // return;
+}
 
-  loadStrudelapps() {
-    this.strudelapps = this.strudelappsService.getStrudelapps()
-  }
 
 
-  openDialog(msg: string ): void {
-      let dialogRef = this.dialog.open(DialogPlaceholder, {
-        width: '250px',
-        height: '400px',
-        data: { 'msg': msg }
-      });
 
-      dialogRef.afterClosed().subscribe(result => {
-        console.log('The dialog was closed');
-      });
+  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');
+  }
 
+  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)});
+  }
 
-@Component({
-  selector: 'dialog-placeholder',
-  templateUrl: 'dialog-placeholder.html',
-})
-export class DialogPlaceholder {
 
-  constructor(
-    public dialogRef: MatDialogRef<DialogPlaceholder>,
-    @Inject(MAT_DIALOG_DATA) public data: any) { }
 
-  onNoClick(): void {
-    this.dialogRef.close();
-  }
+  // 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
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/logindialog/logindialog.component.html b/src/app/logindialog/logindialog.component.html
new file mode 100644
index 0000000..e83844b
--- /dev/null
+++ b/src/app/logindialog/logindialog.component.html
@@ -0,0 +1,3 @@
+
+  <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
new file mode 100644
index 0000000..39047f4
--- /dev/null
+++ b/src/app/logindialog/logindialog.component.spec.ts
@@ -0,0 +1,25 @@
+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
new file mode 100644
index 0000000..b961709
--- /dev/null
+++ b/src/app/logindialog/logindialog.component.ts
@@ -0,0 +1,31 @@
+import { Component, OnInit, Inject  } from '@angular/core';
+import { MatDialog, MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material';
+import { TesService } from '../tes.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) {
+   }
+
+  ngOnInit() {
+  }
+
+  onLogin() {
+    this.tesService.login();
+  }
+  onCancel() {
+    this.dialogRef.close();
+  }
+
+
+}
diff --git a/src/app/logoutdialog/logoutdialog.component.css b/src/app/logoutdialog/logoutdialog.component.css
new file mode 100644
index 0000000..e69de29
diff --git a/src/app/logoutdialog/logoutdialog.component.html b/src/app/logoutdialog/logoutdialog.component.html
new file mode 100644
index 0000000..b1d2c8a
--- /dev/null
+++ b/src/app/logoutdialog/logoutdialog.component.html
@@ -0,0 +1,3 @@
+
+<button  mat-button  (click)="onLogout()">Logout of everything</button>
+<button  mat-button  (click)="onCancel()">Cancel</button>
diff --git a/src/app/logoutdialog/logoutdialog.component.spec.ts b/src/app/logoutdialog/logoutdialog.component.spec.ts
new file mode 100644
index 0000000..15d64c3
--- /dev/null
+++ b/src/app/logoutdialog/logoutdialog.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LogoutdialogComponent } from './logoutdialog.component';
+
+describe('LogoutdialogComponent', () => {
+  let component: LogoutdialogComponent;
+  let fixture: ComponentFixture<LogoutdialogComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ LogoutdialogComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(LogoutdialogComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/app/logoutdialog/logoutdialog.component.ts b/src/app/logoutdialog/logoutdialog.component.ts
new file mode 100644
index 0000000..53a5c8a
--- /dev/null
+++ b/src/app/logoutdialog/logoutdialog.component.ts
@@ -0,0 +1,30 @@
+import { Component, OnInit, Inject } from '@angular/core';
+import { TesService } from '../tes.service';
+import { MatDialog, MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material';
+
+
+@Component({
+  selector: 'app-logoutdialog',
+  templateUrl: './logoutdialog.component.html',
+  styleUrls: ['./logoutdialog.component.css']
+})
+export class LogoutdialogComponent implements OnInit {
+
+  constructor(
+    public dialogRef: MatDialogRef<LogoutdialogComponent>,
+    @Inject(MAT_DIALOG_DATA) public data: any,
+    private tesService: TesService) {
+   }
+
+  ngOnInit() {
+  }
+
+  onLogout() {
+    this.tesService.logout();
+    this.dialogRef.close();
+  }
+  onCancel() {
+    this.dialogRef.close();
+  }
+
+}
diff --git a/src/app/mock-compute-site.ts b/src/app/mock-compute-site.ts
index c2a9b36..26e6050 100644
--- a/src/app/mock-compute-site.ts
+++ b/src/app/mock-compute-site.ts
@@ -1,7 +1,9 @@
 import { Computesite } from './computesite';
+import { STRUDELAPPS } from './mock-strudel-app';
 
 export const COMPUTESITES: Computesite[] = [
-  { url: 'http://localhost:8080/config', name: 'M3',
+  { url: 'https://vm-118-138-240-255.erc.monash.edu.au/m3siteconfig/', name: 'M3',
   host: 'm3.massive.org.au',
-  cafingerprint: 'RSA SHA256:cmDxHrZQSPlBMUUcI/BWmruXho1XOzfXPDHSqVTwV2I' },
+  cafingerprint: 'RSA SHA256:cmDxHrZQSPlBMUUcI/BWmruXho1XOzfXPDHSqVTwV2I',
+  appCatalog: STRUDELAPPS }
 ];
diff --git a/src/app/mock-strudel-app.ts b/src/app/mock-strudel-app.ts
index 5045b19..3feadbf 100644
--- a/src/app/mock-strudel-app.ts
+++ b/src/app/mock-strudel-app.ts
@@ -1,4 +1,4 @@
-import { Strudelapp } from './strudelapp';
+import { Strudelapp } from './computesite';
 
 const tmuxscript = `#!/bin/bash
 #SBATCH -J tmux
@@ -55,10 +55,10 @@ const tmuxclient = {'cmd': ['/usr/bin/gnome-terminal', '--','ssh','-t',
 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: '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 },
+  // { url: null, name: 'Standard Desktop', startscript: stddesktop,
+  //   paramscmd: '/home/chines/desktop_params.py', client: vncviewer, localbind: false },
 ];
diff --git a/src/app/strudelapps.service.ts b/src/app/strudelapps.service.ts
index 8b767a3..30febe9 100644
--- a/src/app/strudelapps.service.ts
+++ b/src/app/strudelapps.service.ts
@@ -2,7 +2,9 @@ import { Injectable } from '@angular/core';
 import { Strudelapp } from './strudelapp';
 import { STRUDELAPPS } from './mock-strudel-app';
 
-@Injectable()
+@Injectable({
+  providedIn: 'root',
+})
 export class StrudelappsService {
 
   constructor() { }
@@ -11,15 +13,6 @@ export class StrudelappsService {
   }
 
   getApp(name: string) {
-    if (name.match(/mux/)) {
-      return STRUDELAPPS[0];
-    }
-    if (name.match(/upyter/)) {
-      return STRUDELAPPS[1];
-    }
-    if (name.match(/desktop/)) {
-      return STRUDELAPPS[2];
-    }
     return STRUDELAPPS[0];
   }
 
diff --git a/src/app/tes.service.ts b/src/app/tes.service.ts
index 66f8ca3..0362dea 100644
--- a/src/app/tes.service.ts
+++ b/src/app/tes.service.ts
@@ -5,9 +5,11 @@ 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 { Strudelapp } from './strudelapp';
 import { Computesite } from './computesite';
-import { Identity } from './identity';
+import { Identity, AuthToken, KeyCert, AuthService } from './identity';
 import { BatchInterface} from './batchinterface';
 import {ComputesitesService} from './computesites.service';
 import { StrudelappsService } from './strudelapps.service';
@@ -17,25 +19,28 @@ import {LocationStrategy} from '@angular/common';
 // import { keypair } from 'keypair';
 import * as keypair from 'keypair';
 import * as forge from "node-forge";
-import { ActivatedRoute } from '@angular/router';
+import { ActivatedRoute, Router } from '@angular/router';
 
 // import keypair = require('keypair');
 // import forge = require('node-forge');
 
 
-@Injectable()
+@Injectable({
+  providedIn: 'root',
+})
 export class TesService {
-private Base='http://localhost:5000'
-//private Base='https://vm-118-138-240-255.erc.monash.edu.au/tes'
-public authorised: BehaviorSubject<boolean>;
-public tesSelected: BehaviorSubject<boolean>;
+// private Base='http://localhost:5000'
+private Base='https://vm-118-138-240-255.erc.monash.edu.au/tes'
+// public authorised: BehaviorSubject<boolean>;
+// public tesSelected: BehaviorSubject<boolean>;
 public statusMsg: BehaviorSubject<any>;
 public jobs: any[];
 public busy: BehaviorSubject<boolean> ;
-public testingAuth: BehaviorSubject<boolean>;
-public joblist: BehaviorSubject<Job[]>;
+// public testingAuth: BehaviorSubject<boolean>;
+public joblist: BehaviorSubject<{ [id: string ]: Job[]}>;
 private timerSubscription: any;
-private token: string;
+private token: Subject<AuthToken>;
+private keyCert: Subject<KeyCert>;
 private signing_endpoint: string;
 private sshauthzbase: string;
 private sshauthz: string;
@@ -43,42 +48,42 @@ private client_id: string;
 private keypair: any;
 private ssh: any;
 private sshcert: any;
-private identities: Identity[];
-private batchinterface: BatchInterface;
+public identities: BehaviorSubject<Identity[]>;
+private batchinterface: {[id: string] : BatchInterface};
 
 
 
   constructor(private http: HttpClient,
               private locationStrategy: LocationStrategy,
               private route: ActivatedRoute,
+              private router: Router,
               private computesite: ComputesitesService,
               private strudelappsService: StrudelappsService) {
     this.statusMsg = new BehaviorSubject<any>('');
-    this.authorised = new BehaviorSubject<boolean>(false);
-    this.tesSelected = new BehaviorSubject<boolean>(true);
-    this.testingAuth = new BehaviorSubject<boolean>(false);
     this.busy = new BehaviorSubject<boolean>(false);
-    this.joblist = new BehaviorSubject<Job[]>([]);
+    this.joblist = new BehaviorSubject<{[id: string]: Job[]}>({});
     this.timerSubscription = null;
-    this.token = null;
-    this.sshauthzbase = "https://autht.massive.org.au/hpcid/";
-    this.sshauthz = this.sshauthzbase + 'oauth/authorize';
-    this.signing_endpoint = this.sshauthzbase + 'api/v1/sign_key';
-    this.client_id = "86c06039-e589-4b39-9d1f-9eca431be18f";
-    this.route.fragment.subscribe(frag => this.getCert(frag));
-    this.batchinterface = new BatchInterface();
-    this.identities=[];
-    this.batchinterface = { cancelcmd: 'scancel {jobid}', statcmd: '/home/chines/jsonstat.py',
-                            submitcmd: 'sbatch --partition=m3f', internalfirewall: false }
+    this.token = new Subject<AuthToken>();
+    this.keyCert = new Subject<KeyCert>();
+    this.identities= new BehaviorSubject<Identity[]>([]);
+    this.route.fragment.subscribe(frag => this.storeToken(frag));
+    this.token.subscribe(token => this.getCert(token));
+    this.keyCert.subscribe(keyCert => this.sshAdd(keyCert));
+    this.batchinterface = {};
+    this.getIdentities();
  }
 
 
-  updateJoblist(resp) {
+  updateJoblist(resp, identity: Identity) {
     let joblist = <Job[]>resp;
+    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);
+      j.identity = identity;
     }
-    this.joblist.next(joblist);
+    alljobs[identity.repr()] = joblist;
+    this.joblist.next(alljobs);
     this.statusMsg.next(null);
   }
 
@@ -96,6 +101,7 @@ private batchinterface: BatchInterface;
    params.set('interface',interfacestr);
    let identitystr = JSON.stringify(identity);
    params.set('identity',identitystr);
+
    return params.toString();
  }
 
@@ -103,27 +109,52 @@ private batchinterface: BatchInterface;
    let headers = new HttpHeaders();
    let options = { headers: headers, withCredentials: true};
    this.statusMsg.next(null);
-   for (let identity of this.identities) {
-      let paramstr = this.buildParams(null,identity,this.batchinterface);
-      this.http.get<Job[]>(this.Base+'/stat'+'?'+paramstr,options)
+   // 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);
+   let ids = []
+   for (let identity of this.identities.value) {
+     ids.push(identity.repr());
+   }
+   for (let id of oldjobkeys) {
+     if (!ids.includes(id)) {
+       delete oldjobs[id];
+     }
+   }
+   this.joblist.next(oldjobs);
+   // for identities that we do know about, query the job list
+   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);
+        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));
+                      .subscribe(resp => this.updateJoblist(resp,identity));
                     }
+
  }
 
+ getconfig(app: Strudelapp, identity: Identity): Observable<any> {
+   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',[])))
 
- submit(app: Strudelapp, identity: Identity) {
-   console.log("In tes submit url "+this.Base+'/submit');
-   console.log("starting app"+app.name);
+
+}
+
+ submit(app: Strudelapp, identity: Identity, batchinterface: BatchInterface) {
    let headers = new HttpHeaders();
    let options = { headers: headers, withCredentials: true};
    this.statusMsg.next('Submitting job');
    this.busy.next(true);
-   let paramstr = this.buildParams(app,identity,this.batchinterface);
+   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));
-
  }
 
  cancel(job: Job) {
@@ -132,10 +163,13 @@ private batchinterface: BatchInterface;
    let options = { headers: headers, withCredentials: true};
    this.statusMsg.next(null);
    let data = {};
-   let paramstr = this.buildParams(job.app,job.identity,this.batchinterface);
+   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',[])))
                       .subscribe(resp => this.submitted(resp));
+    console.log('tes.cancel complete');
  }
 
  public connect(job: Job) {
@@ -143,81 +177,112 @@ private batchinterface: BatchInterface;
    let headers = new HttpHeaders();
    let options = { headers: headers, withCredentials: true};
    this.busy.next(true);
-   let paramstr = this.buildParams(job.app,job.identity,this.batchinterface);
+   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();
    this.busy.next(false);
  }
 
 
+storeToken(frag: string) {
+  if (frag === undefined || frag == null) {
+      return;
+  }
+  let tokenmatch = null;
+  let statematch = null;
+  if (!(frag === undefined) && !(frag == null)) {
+    tokenmatch = frag.match(/access_token\=([\S\s]*?)[&|$]/);
+    statematch = frag.match(/state\=([\S\s]*?)[&|$]/);
+  }
+  if (tokenmatch == null || statematch == null) {
+    return;
+  }
+
+  let accesstoken = tokenmatch[1];
+  let state = statematch[1];
+  this.router.navigate(['/']);
+
+  //Verify that the state matched the nonce we used when initiating login
+  let tuple = JSON.parse(localStorage.getItem('authservice'));
+  if (tuple[1] != state) {
+    return
+  }
+
+  this.token.next(new AuthToken(tokenmatch[1],tuple[0]));
+
+  // TODO fire off a query to the auth service to get the associated sites
 
-getCert(frag: string) {
-   // Given a URL fragment, extract the authentication token, and use it to get
-   // an SSH certificate. SSH cert to be used on subsequent calls to the TES
-   console.log('in tesservice getCert argument is',frag);
-   console.log('fragment.getvalue is')
-   console.log(frag)
-   let match = frag.match(/access_token\=([\S\s]*?)[&|$]/);
-   if (match == null) {
-     return;
+
+}
+
+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');
+     console.log(token);
+     return
    }
-   this.authorised.next(true);
-   console.log(match);
-   this.token = match[1];
-   console.log(this.token);
-   // this.token = frag;
-   console.log('tes stored the token'+this.token);
-
-   // let data = {'token': this.token, 'sshauthz_endpoint': this.signing_endpoint}
-   // console.log(data);
-   console.log("Generating key matter")
+   console.log("Generating key matter");
+
    let starttime = new Date();
-   this.keypair = keypair();
-   let publicKey = forge.pki.publicKeyFromPem(this.keypair.public);
-   this.ssh = forge.ssh.publicKeyToOpenSSH(publicKey, 'sv2@monash.edu');
+   let newkeypair = keypair();
+   let publicKey = forge.pki.publicKeyFromPem(newkeypair.public);
+   let sshpub = forge.ssh.publicKeyToOpenSSH(publicKey, 'sv2@monash.edu');
    let endtime = new Date();
    console.log("generating new keys took", endtime.valueOf() - starttime.valueOf())
 
    let headers = new HttpHeaders();
    let options = { headers: headers, withCredentials: true};
 
-   let data = {'token': this.token, 'pubkey': this.ssh, 'signing_url': this.signing_endpoint};
+   let data = {'token': token.token, 'pubkey': sshpub, 'signing_url': token.authservice.sign};
 
    this.busy.next(true);
    this.statusMsg.next("Generating Certificates ...")
    console.log('posting to getcert',this.Base);
    this.http.post<any>(this.Base+'/getcert',data, options)
                       .pipe(catchError(this.handleError('getCert',[])))
-                      .subscribe(resp => this.storeCert(resp));
+                      .subscribe(resp => this.makeKeyCert(newkeypair.private, resp, token.authservice))
+  console.log('getcert complete');
+}
+
+makeKeyCert(key: string, resp, authservice: AuthService) {
+  let keyCert = new KeyCert()
+  keyCert.key = key;
+  keyCert.cert = resp['cert'];
+  keyCert.authservice = authservice;
+  console.log('updating keycert',keyCert);
+  this.keyCert.next(keyCert);
 }
 
-private storeCert(resp) {
-  console.log(resp);
-  this.busy.next(false);
-  this.authorised.next(true);
-  this.testingAuth.next(false);
+private sshAdd(keyCert: KeyCert) {
 
-  console.log(resp['cert']);
-  this.sshcert = resp['cert'];
+  console.log('in sshAdd');
+  if (keyCert.key == undefined) {
+    return;
+  }
   let headers = new HttpHeaders();
   let options = { headers: headers, withCredentials: true};
 
   this.statusMsg.next("Authorising ...")
-  let data = {'key': this.keypair.private, 'cert': this.sshcert};
+  let data = {'key': keyCert.key, 'cert': keyCert.cert};
+  console.log('adding key',data);
 
   this.http.post<any>(this.Base+'/sshagent',data,options)
                       .pipe(catchError(this.handleError('storeCert',[])))
-                      .subscribe(resp => this.getIdentities(resp));
+                      .subscribe(resp => this.getIdentities(),
+                                error => this.httperror(error));
+  console.log('sshAdd complete');
 }
 
-private getIdentities(resp) {
+private getIdentities() {
+  console.log('retrieving current identities');
   this.statusMsg.next("Updating the list of available accounts")
-  this
   let headers = new HttpHeaders();
   let options = { headers: headers, withCredentials: true};
   this.http.get<any>(this.Base+'/sshagent',options)
-                    .pipe(catchError(this.handleError('updateIdentities',[])))
+                    .pipe(catchError(this.handleError('getIdentities',[])))
                     .subscribe(resp => this.updateIdentities(resp));
+  console.log('getIdentities complete');
 }
 
 private killAgent() {
@@ -226,32 +291,35 @@ private killAgent() {
   let headers = new HttpHeaders();
   let options = { headers: headers, withCredentials: true};
   this.http.delete<any>(this.Base+'/sshagent',options)
-                    .pipe(catchError(this.handleError('updateIdentities',[])))
+                    .pipe(catchError(this.handleError('killAgent',[])))
                     .subscribe(resp => this.updateIdentities(resp));
 }
 
 private updateIdentities(resp) {
+  //TODO Each cert as the signing CA parameter. Use this to find the compute sites
+  // rather than just assuming sites[0]
+  console.log('attempting to update our local list of identities');
   let certcontents = resp;
   console.log(certcontents);
-  this.identities = [];
+  let identities = [];
   console.log('identities updated');
   let sites = this.computesite.getComputeSites();
+  let idsShort = []
   for (let i in certcontents) {
-    let id = new Identity()
-    id.username = certcontents[i].Principals[0];
-    id.site = sites[0]
-    this.identities.push(id)
+    identities.push(new Identity(certcontents[i].Principals[0],sites[0]))
   }
-  if (this.identities.length == 0) {
+  this.identities.next(identities);
+  if (identities.length == 0) {
     this.statusMsg.next(null);
+    console.log('local identities, none available');
     return;
   }
-  console.log(this.identities);
-  this.statusMsg.next('Updating Job list');
+  console.log('local identities updated');
   this.startPolling();
 }
 
 private startPolling() {
+  console.log('in start polling');
   this.statusMsg.next(null);
   if (!(this.timerSubscription === null)) {
     console.log('unsubscribing timer');
@@ -262,23 +330,29 @@ private startPolling() {
 
   this.timerSubscription = timer(5000).pipe(repeat()).subscribe(() => this.getJobs());
   this.getJobs();
+  console.log('start polling complete');
 }
 
 public login() {
   let redirect_uri = window.location.origin+this.locationStrategy.getBaseHref()+"sshauthz_callback";
-  let state="asdfzxcv";
-  window.location.assign(this.sshauthz+"?response_type=token&redirect_uri="+redirect_uri+"&state="+state+"&client_id="+this.client_id);
+  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);
 }
 
 public logout(): Boolean {
   this.token = null;
   this.sshcert = null;
   this.statusMsg.next(null);
-  this.authorised.next(false);
   this.killAgent();
-  if (!(this.timerSubscription === null)) {
-    this.timerSubscription.unsubscribe()
-  }
+  // if (!(this.timerSubscription === null)) {
+  //   this.timerSubscription.unsubscribe()
+  // }
   return true;
 }
 
@@ -287,20 +361,23 @@ public getstatusMsgSubject(): BehaviorSubject<any> {
   return this.statusMsg;
 }
 
+private httperror(error: any) {
+  console.log(error);
+}
 
 private handleError<T> (operation = 'operation', result?: T) {
   return (error: any): Observable<T> => {
-    // TODO: send the error to remote logging infrastructure
-    console.error(error); // log to console instead
-    this.authorised.next(false);
-    this.token = null;
-    this.sshcert = null;
+    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.");
-    }
-    if (operation == 'submit') {
+    } 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);
     }
     return of(result as T);
   };
diff --git a/src/app/teserrors/teserrors.component.ts b/src/app/teserrors/teserrors.component.ts
index 068b8cd..607b7d1 100644
--- a/src/app/teserrors/teserrors.component.ts
+++ b/src/app/teserrors/teserrors.component.ts
@@ -13,11 +13,17 @@ export class TeserrorsComponent implements OnInit {
   constructor(private tesService: TesService,) { }
 
   ngOnInit() {
+    console.log('constructing message component');
     this.tesService.getstatusMsgSubject().subscribe( msg => this.getNewMessage(msg));
+    console.log('constructing message component complete');
+
   }
 
   getNewMessage(msg) {
+    console.log('teserrors get message');
     this.statusMsg = msg;
+    console.log('teserrors get message complete');
+
   }
 
 }
-- 
GitLab