Commit 8fb5e678 authored by Lance Wilson's avatar Lance Wilson
Browse files

Merge branch 'new_css_dev' into 'master'

New css dev

Closes #30

See merge request !21
parents 0946e908 fcf6e07b
Pipeline #8422 passed with stage
in 3 minutes and 37 seconds
......@@ -4,16 +4,18 @@ stages:
image: ubuntu
- build
stage: build
- apt update
- apt install -y curl gnupg
- curl -sL | bash -
- curl -sL | bash -
- apt update
- apt install -y nodejs
- rm ./package-lock.json
- npm install
- ./node_modules/@angular/cli/bin/ng build --prod --base-href=/sv2/
- ./node_modules/@angular/cli/bin/ng build --prod --base-href=/ --configuration=$CI_COMMIT_REF_NAME
- ./dist/
......@@ -21,7 +21,8 @@
"styles": [
"scripts": []
......@@ -42,6 +43,30 @@
"with": "src/environments/"
"test": {
"fileReplacements": [
"replace": "src/assets/config/apiservers.json",
"with": "src/assets/config/apiservers.test.json"
"replace": "src/assets/config/computesites.json",
"with": "src/assets/config/computesites.test.json"
"dev": {
"fileReplacements": [
"replace": "src/assets/config/apiservers.json",
"with": "src/assets/config/"
"replace": "src/assets/config/computesites.json",
"with": "src/assets/config/"
......@@ -129,4 +154,4 @@
"prefix": "app"
\ No newline at end of file
Adding an appliction
You need to define a program to run and put the value in `startscript`. You can't use #SBATCH pragmas here though.
You also need to define a program that will take a jobid and return the port the program is running on and anything else (like access tokens or passwords). This is a paramscmd. The paramscmd can return a json error message. The paramscmd might look easy and a proof of concept might take 5 minutes, but then you get into edge cases and it turns out to be the hardest bit so try to copy an existing one.
Next you need the URL to connect to (eg index.html?token=adsf). This is a realtive URL (i.e. if you would normally use ssh tunnels and localhost:<nnn> drop the localhost part
Finally you put all this data into a json structure and save it to the config files.
The config file
Strudel applications look like this
"url": null,
"name": "Jupyter Lab",
"startscript": "#!/bin/bash\n/usr/local/sv2/dev/jupyter/jupyter.slurm\n",
"actions": [
"name": "Connect",
"paramscmd": "/usr/local/sv2/dev/jupyter/ {jobid}",
"client": {"cmd": null, "redir": "?token={token}"},
"states": ["RUNNING"]
"name": "View log",
"paramscmd": "/usr/local/sv2/dev/desktop/ {jobid}",
"client": {"cmd": null, "redir": "index.html?token={token}" },
"states": ["RUNNING","Finished"]
"name": "View Usage",
"paramscmd": "/usr/local/sv2/dev/desktop/ {jobid}",
"client": {"cmd": null, "redir": "index.html?token={token}" },
"states": ["Finished"]
"name": "Remove log",
"paramscmd": "/usr/local/sv2/dev/ {jobid}",
"client": null,
"states": ["Finished"]
"localbind": true,
"applist": null
This is block of json data. It gets stored in a config file. Each compute site has its own list of applications. And if you are running dev and test environments you probably have a different set of applications on each. For M3 the applications are deployed as part of the frontend build (because it was easy to combine the config and the code) but for other sites the URL for this configuration data might be completely independent. For M3 the applications deployed to dev are defined here
and the applications for test are here
In order to deploy a new application you sould edit those files on dev and create a merge request
Whats are all these keys and values
The first value `url` allows up to specify a URL that will provide additional configuration info. We don't use this for desktops or Jupyter but we might use it for transfering files. The URL should open in an iframe and use window.message methods to pass data back to Strudel2. For most applications this will be set to None.
the `name` value is reasonably self explinatory but its worth noting: When the form is rendered for what resources to use (CPUs/GPUs/Time) this value is passed to the form so that for example the application named "Desktop" renders a different form than the applictaion named "Jupyter Lab". Don't the M3 forms will render a good default for any unknown applicaiton names, so you can pretty much fill in whatever you like.
The `startscript` gets passed as stdin to whatever command the site runs things with. In the case of M3 this is sbatch, so this contents (start script) gets passed to sbatch. In this example `/usr/local/sv2/dev/jupyter/jupyter.slurm` i *NOT* a slurm script. i.e. if you put #SBATCH lines in there they will be ignored. It is a program. If you need to do #SBATCH lines it should be like
`"startscript": "#!/bin/bash\n#SBATCH -w m3a011\n/usr/local/sv2/dev/jupyter/jupyter.slurm\n",` but you really shouldn't do this. The intention is that the start script doesn't care what job scheduler your using
Next we have a list of `actions`. Each of these renders a button in Strudel2 depending on the state of the Job. For each action what happens is tha the the `paramscmd` gets run and returns a blob of json data. Then the client is executed using the data returned from the `paramscmd`. The paramscmd should always return a value for `port` i.e. the network port to connect on and should also return any info used in the `client` definition (so `` must return both a port and a token like `{"port":123,"token":"abc"}`) You sould attempt to copy your params cmd off an existing implementation. Ideally they are not aware of what batch environment is used so that a `paramscmd` used on a PBS site can be shared with a slurm site. In practice we've had to make our paramcmds aware of the jobid and the process tree origining from that job id in order to connect to the correct job (incase multiple jobs are running on the same node)
You'll notice tha tthe `client` defines both a `cmd` and a `redir`. The `cmd` field is reserved for future work where Strudle2 can be installed locally and use things like a native vnc viewer instead of a web browser and noVNC.
Next we have the `localbind` option. This should be set to true. It controls the behaviour of tunnels. In particular you generally can't access Jupyter from the login node, you have to ssh to the execution host and access using `ssh -L 8888:localhost:8888 <exechost>` On the other hand if you want to access the SSH server on the execution host you don't need to do `ssh -L 2222:localhost:22 <exechost>` you can get stright there from the login node.
Finally the `applist` option allows for recursivly nexting another list of apps. I implement this feature in the S2 UI, but because its not currently in use and has no test coverage its probably got some bit rot. If you feel the need to have a multilevel list of applications, please contact the developer.
This diff is collapsed.
......@@ -12,45 +12,45 @@
"private": true,
"dependencies": {
"@angular/animations": "7.2.4",
"@angular/cdk": "^7.3.1",
"@angular/common": "7.2.4",
"@angular/compiler": "7.2.4",
"@angular/core": "7.2.4",
"@angular/flex-layout": "^7.0.0-beta.23",
"@angular/forms": "7.2.4",
"@angular/http": "7.2.4",
"@angular/material": "^7.3.1",
"@angular/platform-browser": "7.2.4",
"@angular/platform-browser-dynamic": "7.2.4",
"@angular/router": "7.2.4",
"core-js": "^2.6.4",
"@angular/animations": "8.0.0",
"@angular/cdk": "^8.0.0",
"@angular/common": "8.0.0",
"@angular/compiler": "8.0.0",
"@angular/core": "8.0.0",
"@angular/flex-layout": "^8.0.0-beta.26",
"@angular/forms": "8.0.0",
"@angular/http": "7.2.15",
"@angular/material": "^8.0.0",
"@angular/platform-browser": "8.0.0",
"@angular/platform-browser-dynamic": "8.0.0",
"@angular/router": "8.0.0",
"core-js": "^3.1.3",
"hammerjs": "^2.0.8",
"keypair": "^1.0.1",
"node-forge": "^0.8.0",
"rxjs": "^6.4.0",
"rxjs-compat": "^6.4.0",
"zone.js": "^0.8.29"
"node-forge": "^0.8.4",
"rxjs": "^6.5.2",
"rxjs-compat": "^6.5.2",
"zone.js": "^0.9.1"
"devDependencies": {
"@angular/cli": "7.2.4",
"@angular/compiler-cli": "7.2.4",
"@angular/language-service": "7.2.4",
"@types/jasmine": "~3.3.8",
"@angular-devkit/build-angular": "^0.800.1",
"@angular/cli": "8.0.1",
"@angular/compiler-cli": "8.0.0",
"@angular/language-service": "8.0.0",
"@types/jasmine": "~3.3.13",
"@types/jasminewd2": "~2.0.6",
"@types/node": "~10.12.24",
"codelyzer": "^4.5.0",
"jasmine-core": "~3.3.0",
"@types/node": "~12.0.5",
"codelyzer": "^5.1.0",
"jasmine-core": "~3.4.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~4.0.0",
"karma": "^4.1.0",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "^2.0.4",
"karma-coverage-istanbul-reporter": "^2.0.5",
"karma-jasmine": "~2.0.1",
"karma-jasmine-html-reporter": "^1.4.0",
"karma-jasmine-html-reporter": "^1.4.2",
"protractor": "~5.4.2",
"ts-node": "~8.0.2",
"tslint": "~5.12.1",
"typescript": "3.2.4",
"@angular-devkit/build-angular": "~0.13.1"
"ts-node": "~8.2.0",
"tslint": "~5.17.0",
"typescript": "3.4.5"
.table-header {
border: 1px solid #a2bae1;
background-color: #f0f0f0;
width: 100%;
padding-top: 10px;
padding-bottom: 10px;
.table-header span {
margin-left: 10px;
color: #1a50a5;
font-family: Raleway;
font-size: 22px;
.mat-table {
font-family: Raleway;
background-color: #fafafa;
.mat-header-cell {
color: #11356e;
font-size: 18px;
\ No newline at end of file
<div fxLayout=column fxLayoutAlign="space-between" style="width: 100%; height: 100%">
<div *ngIf="identity$.value !== null && identity$.value !== undefined">
<!--<div *ngIf="identity$.value.systemalerts.value !== null">-->
<div *ngFor="let h of (identity$.value.systemalerts | async)">
<div *ngIf="h.stat == 'error'">
<div class='health-warn'>
{{ h.msg }}
<div *ngIf="((identity$ | async).accountalerts | async) ; else loadingaa">
<div *ngFor="let h of identity$.value.accountalerts.value">
<div *ngIf="h.type === undefined || h.type != 'quota'">
<div [ngClass]="h.stat == 'error' || h.stat == 'warn' ? 'health-warn': 'health-ok'">
{{ h.msg }}
<div *ngIf="h.type == 'table'">
<div class="table-header"><span>{{ h.title }}</span></div>
<table mat-table [dataSource]="" style="width: 100%">
<ng-container *ngFor="let c of" matColumnDef="{{c.key}}">
<th mat-header-cell *matHeaderCellDef style="text-align: left">{{ c.header }} </th>
<td mat-cell *matCellDef="let row;" style="text-align: left"> {{row[c.key]}}</td>
<tr mat-header-row *matHeaderRowDef="calculateCols(; sticky: true" ></tr>
<tr mat-row *matRowDef="let row; columns: calculateCols(" [ngClass]="rowClass(row)" ></tr>
<ng-template #loadingaa>
<h2> Loading account info...</h2>
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AccountinfoComponent } from './accountinfo.component';
describe('AccountinfoComponent', () => {
let component: AccountinfoComponent;
let fixture: ComponentFixture<AccountinfoComponent>;
beforeEach(async(() => {
declarations: [ AccountinfoComponent ]
beforeEach(() => {
fixture = TestBed.createComponent(AccountinfoComponent);
component = fixture.componentInstance;
it('should create', () => {
import { Component, OnInit, Input } from '@angular/core';
import { Identity } from '../identity';
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
import { TesService } from '../tes.service';
import { BehaviorSubject, Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';
import {Strudelapp} from '../strudelapp';
import {Health} from '../computesite';
selector: 'app-accountinfo',
templateUrl: './accountinfo.component.html',
styleUrls: ['./accountinfo.component.css']
export class AccountinfoComponent implements OnInit {
@Input() identity$: BehaviorSubject<Identity>;
private subscriptions: Subscription[];
private aa: Health[];
private sa: Health[];
@Input() app$: BehaviorSubject<Strudelapp>;
private router: Router,
public tesService: TesService,
) {
this.subscriptions = [];
this.aa = []; = [];
ngOnInit() {
console.log('account info initialised');
this.identity$.subscribe((i) => this.updateSubs(i));
updateSubs(i: Identity) {
console.log('identity changed, updating subscriptions');
var s: Subscription;
for (s of this.subscriptions) {
console.log('calling unsub');
if (i === null) {
console.log('id is actually null nothing to subscribe');
this.subscriptions.push(i.accountalerts.subscribe((v) => {this.aa = v ; console.log('updated aa'); console.log(i.displayName()); console.log(v);}));
this.subscriptions.push(i.systemalerts.subscribe((v) => { = v ; console.log('updated sa'); console.log(i.displayName()); console.log(v)}));
calculateCols(data) {
return { return r.key });
humanKBytes(n: number) {
if (n > 1024*1024*1024) {
let v = n/1024/1024/1024;
return v.toFixed(0)+'TB';
if (n > 1024*1024) {
let v = n/1024/1024;
return v.toFixed(0)+'GB';
if (n > 1024) {
let v = n/1024;
return v.toFixed(0)+'MB';
return '0 MB';
rowClass(row) {
if (row.stat == 'error' || row.stat == 'warn') {
return 'health-warn';
} else {
return 'health-ok';
navLogin(o) {
if (o == null) {
if (o.length == 0) {
} else {
......@@ -4,6 +4,11 @@ import { LauncherComponent } from './launcher/launcher.component';
import { KeygenComponent } from './keygen/keygen.component';
import { TransferComponent } from './transfer/transfer.component';
import { ShareconnectComponent } from './shareconnect/shareconnect.component';
import { JoblistComponent } from './joblist/joblist.component';
import {LoginComponent} from './login/login.component';
import {LogoutComponent} from './logout/logout.component';
import {SettingsComponent} from './settings/settings.component';
// import { TokenextractorComponent } from './tokenextractor/tokenextractor.component';
......@@ -11,12 +16,18 @@ import { ShareconnectComponent } from './shareconnect/shareconnect.component';
const routes: Routes = [
{ path: '', redirectTo: 'launch', pathMatch: 'full'},
//{ path: 'launch', component: JoblistComponent},
{ path: 'launch', component: LauncherComponent},
{ path: 'finishlaunch', component: LauncherComponent},
{ path: 'cancellaunch', component: LauncherComponent},
{ path: 'launch/:site', component: LauncherComponent},
{ path: 'launch/:site/:app', component: LauncherComponent},
{ path: 'login', component: LoginComponent},
{ path: 'logout', component: LogoutComponent},
{ path: 'settings', component: SettingsComponent },
// { path: 'finishlaunch', component: LauncherComponent},
//{ path: 'cancellaunch', component: LauncherComponent},
{ path: 'sshauthz_callback', component: KeygenComponent},
{ path: 'transfer', component: TransferComponent },
{ path: 'shareconnect', component: ShareconnectComponent }
//{ path: 'shareconnect', component: ShareconnectComponent }
// { path: 'sshauthz_callback', component: LauncherComponent}
.side-nav {
margin-left: 0px;
margin-right: 0px;
margin-top: 0px;
margin-bottom: 0px;
.main-content {
height: 100%;
width: 100%;
padding-top: 20px;
.empty-spacer {
margin: 10px;
.mat-button {
font-family: Raleway;
.banner {
letter-spacing: 15px;
font-size: 22px;
font-family: Raleway;
.footer {
background-color: #303030;
color: #f0f0f0;
.profile-button {
padding: 30px 0px 0px 0px;
.button-link {
font-family: Raleway;
font-size: 14px;
padding: 0 30px;
\ No newline at end of file
<!-- <router-outlet class="fill-remaining-space"></router-outlet> -->
<!--<div *ngIf="(tesService.theme | async) == 'strudel-dark-theme'" class="strudel-dark-theme" style="width: 100vw; height: 100vh">
<div *ngIf="(tesService.theme | async) == 'strudel-light-theme'" class="strudel-light-theme" style="width: 100vw; height: 100vh">
</div> -->
<div style="height: 100%" fxLayout="column">
<mat-toolbar color="primary">
<mat-toolbar-row style="height: 80px">
<button mat-icon-button *ngIf="settingsService.useMenu$ | async" aria-label="toggle menu" (click)=toggleMenu()><mat-icon>menu</mat-icon></button>
<span class="empty-spacer"></span>
<span class="banner">STRUDEL</span>
<span fxFlex></span>
<span class="fill-horizontal-space"></span>
<button mat-button [matMenuTriggerFor]="actionmenu" disableRipple="true" class="profile-button">
{{ displayUsername() }}
<mat-menu #actionmenu="matMenu">
<div *ngFor="let az of (authService.loggedInAuthZ | async)">
<button mat-menu-item routerLink="/logout"><mat-icon>logout</mat-icon>Log out of {{ }}</button>
<button mat-menu-item routerLink="/settings"><mat-icon>settings</mat-icon>Settings</button>
<button *ngIf="(authService.loggedOutAuthZ | async).length > 0" mat-menu-item routerLink="/login"><mat-icon>exit_to_app</mat-icon>More services</button>
<div *ngFor="let id of (computeSitesService.appidentities | async)">
<button mat-button fxFlex
routerLinkActive #rla="routerLinkActive"
[routerLinkActiveOptions]="{'exact': false}"
style="text-align: left"
matBadge="{{ countErrors((id.systemalerts | async), (id.accountalerts | async)) }}"
[matBadgeHidden]= "countErrors((id.systemalerts | async), (id.accountalerts | async)) == 0"
matBadgePosition="above before"
matBadgeOverlap="false" matBadgeSize="small"
style="text-align: left; margin-left: 15px; margin-top: 10px">
{{ id.displayName() }}
<mat-sidenav-container fxFlex class="side-nav">
<mat-sidenav closed>
<div fxLayout="column" fxLayoutAlign="start none" class="main-content">
<mat-toolbar class="footer">
<span fxFlex></span>
<button mat-button class="button-link" disableRipple="true" routerLink="/about-us">About Us</button>
<button mat-button class="button-link" disableRipple="true" routerLink="/our-services">Our services</button>
<button mat-button class="button-link" disableRipple=true routerLink="/contact-us">Contact Us</button>
<span fxFlex></span>
......@@ -4,6 +4,10 @@ import { AuthorisationService} from './authorisation.service';
import { ComputesitesService} from './computesites.service';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import { MatSnackBar } from '@angular/material';
import { Computesite, Health } from './computesite';
import {SettingsService } from './settings.service';
import {OverlayContainer} from '@angular/cdk/overlay';
......@@ -23,8 +27,10 @@ export class AppComponent {
constructor(private tesService: TesService,
private authService: AuthorisationService,
private computesitesService: ComputesitesService,
public snackBar: MatSnackBar,) {
private computeSitesService: ComputesitesService,
private settingsService: SettingsService,
public snackBar: MatSnackBar,
private overlayContainer: OverlayContainer) {
......@@ -33,15 +39,24 @@ export class AppComponent {
// this.testingAuth = false;
this.statusMsg = new BehaviorSubject<any>('');