Commit 6552be3b authored by lukas.burgey's avatar lukas.burgey

Merge branch 'dev'

parents 17278ec6 9d8cd20f
DIST := dist HOME := /home/feudal
DEST := $(HOME)/www
NM := node_modules NM := node_modules
NG := $(NM)/.bin/ng NG := $(NM)/.bin/ng
.PHONY: clean, dev, prod, $(NM) NG_BUILD := $(NG) build --aot --output-path $(DEST)
.PHONY: all, clean, cleaner, dev, watch, prod, watch-prod, $(NM)
all: prod all: prod
...@@ -18,16 +21,16 @@ $(NM): ...@@ -18,16 +21,16 @@ $(NM):
$(NG): $(NM) $(NG): $(NM)
dev: $(NG) dev: $(HOME) $(NG)
@$(NG) build --aot $(NG_BUILD)
watch: $(NG) watch: $(HOME) $(NG)
@$(NG) build --aot --watch $(NG_BUILD) --watch
prod: $(NG) prod: $(HOME) $(NG)
@$(NG) build --aot --prod $(NG_BUILD) --prod
watch-prod: $(NG) watch-prod: $(HOME) $(NG)
@$(NG) build --aot --prod --watch $(NG_BUILD) --prod --watch
$(DIST): prod $(DEST): $(HOME) prod
...@@ -92,31 +92,6 @@ ...@@ -92,31 +92,6 @@
} }
} }
} }
},
"feudal-e2e": {
"root": "",
"sourceRoot": "",
"projectType": "application",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "./protractor.conf.js",
"devServerTarget": "feudal:serve"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"e2e/tsconfig.e2e.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
} }
}, },
"defaultProject": "feudal", "defaultProject": "feudal",
......
This source diff could not be displayed because it is too large. You can view the blob instead.
<header feudal-header></header> <header feudal-header></header>
<div class="body"> <div class="body">
<div *ngIf="(userService.combiSrc() | async) as combi" style="max-width: 800px;" class="centered"> <div *ngIf="(userService.combiSrc() | async) as combi" class="centered">
<div *ngIf="combi.user != undefined"> <div *ngIf="combi.user != undefined">
<div *ngIf="(userService.sshKeysSrc() | async)?.length > 0; else noCredentials"> <div *ngIf="(userService.sshKeysSrc() | async)?.length > 0; else noCredentials">
<div style="margin-bottom: 50px;"> <div style="margin-bottom: 50px;">
......
<mat-expansion-panel> <mat-expansion-panel>
<mat-expansion-panel-header> <mat-expansion-panel-header>
<mat-panel-title style="min-width: 100px;">{{ service.name }}</mat-panel-title> <mat-panel-title style="min-width: 100px;">{{ service.name }}</mat-panel-title>
<mat-panel-description *ngIf="(deployment$ | async) as dep"> <mat-panel-description *ngIf="(deployment$ | async) as dep">
<span *ngIf="dep.state_target == 'deployed'"> <span *ngIf="dep.state_target == 'deployed'">
Access requested Access requested
</span> </span>
</mat-panel-description> </mat-panel-description>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<!-- expansion body --> <!-- expansion body -->
<div> <div>
<table> <table>
<thead> <thead>
<tr> <tr>
<td>Site</td> <td>Site</td>
<td>Service</td> <td>Service</td>
<td>State</td> <td>State</td>
</tr> <td></td>
</thead> <td>Contact</td>
<tbody> </tr>
<tr deployment-state-tr [service]="service"></tr> </thead>
</tbody> <tbody>
</table> <tr deployment-state-tr [service]="service"></tr>
</div> </tbody>
<ng-template #noDeployment> </table>
<p> </div>
Your credentials were never deployed to the service {{ service.name }} <ng-template #noDeployment>
</p> <p>
</ng-template> Your credentials were never deployed to the service {{ service.name }}
<mat-action-row deployment-action-row [deployment$]="deployment$" [service]="service"></mat-action-row> </p>
</ng-template>
<mat-action-row deployment-action-row [deployment$]="deployment$" [service]="service"></mat-action-row>
</mat-expansion-panel> </mat-expansion-panel>
...@@ -2,3 +2,30 @@ ...@@ -2,3 +2,30 @@
.my-tooltip { .my-tooltip {
white-space: pre-line; white-space: pre-line;
} }
h4 {
margin-bottom: 5px;
}
td {
vertical-align: top;
}
.mat-icon {
font-size: 15px;
height: 15px;
width: 15px;
vertical-align: initial;
}
small {
font-size: small;
}
.width-limit {
max-width: 250px;
}
.bold {
font-weight: bold;
}
<!-- site column -->
<td> <td>
<span matTooltip="{{ service.site.description}}\nThis site provides the service {{ service.name }} for you." matTooltipClass="my-tooltip"> <h4>{{ service.site.name }}</h4>
{{ service.site.name }} <br>
<mat-icon>help</mat-icon> <small>{{ service.site.description }}</small>
</span>
</td> </td>
<!-- service column -->
<td> <td>
<span matTooltip="{{ service.description }}"> <h4 style="font-weight: bold;">{{ service.name }}</h4>
{{ service.name }} <br>
<mat-icon>help</mat-icon> <small>{{ service.description }}</small>
</span>
</td> </td>
<ng-container *ngIf="(state$ | async) as state; else noStateItem"> <ng-container *ngIf="(state$ | async) as state; else noStateItem">
<td [matTooltip]="tooltip(state)"> <!-- state column -->
<td class="width-limit">
<span [ngSwitch]="state.state" class="spaced"> <span [ngSwitch]="state.state" class="spaced">
<mat-icon *ngSwitchCase="'deployed'">call_made</mat-icon> <mat-icon *ngSwitchCase="'deployed'">call_made</mat-icon>
<mat-icon *ngSwitchCase="'questionnaire'">warning</mat-icon> <mat-icon *ngSwitchCase="'questionnaire'">warning</mat-icon>
<mat-progress-spinner *ngSwitchCase="'deployment_pending'" diameter="24" mode="indeterminate"></mat-progress-spinner> <mat-progress-spinner *ngSwitchCase="'deployment_pending'" diameter="15" mode="indeterminate"></mat-progress-spinner>
<mat-progress-spinner *ngSwitchCase="'removal_pending'" diameter="24" mode="indeterminate"></mat-progress-spinner> <mat-progress-spinner *ngSwitchCase="'removal_pending'" diameter="15" mode="indeterminate"></mat-progress-spinner>
<mat-icon *ngSwitchCase="'not_deployed'" mat-icon-button>call_received</mat-icon> <mat-icon *ngSwitchCase="'not_deployed'">call_received</mat-icon>
<mat-icon *ngSwitchCase="'failed'" mat-icon-button>error</mat-icon> <mat-icon *ngSwitchCase="'failed'">error</mat-icon>
<mat-icon *ngSwitchCase="'rejected'" mat-icon-button>error</mat-icon> <mat-icon *ngSwitchCase="'rejected'">error</mat-icon>
<mat-icon *ngSwitchDefault mat-icon-button>call_received</mat-icon> <mat-icon *ngSwitchDefault>call_received</mat-icon>
</span> </span>
{{ lang.printState(state.state) }} <h4>{{ lang.printState(state.state) }}</h4>
<br>
<div class="mat-small">{{ stateDescription }}</div>
</td> </td>
<!-- state dependent buttons --> <!-- actions column -->
<td *ngIf="state.state == 'deployed'"> <td>
<button (click)="dialog.openCredentials(state$)" mat-raised-button class="mat-elevation-z6"> <div *ngIf="button !== undefined">
Credentials <button (click)="buttonAction(state)" matTooltip="{{ button.tooltip }}" color="{{ button.color }}" mat-raised-button class="mat-elevation-z6">
</button> {{ button.text }}
</td> </button>
<td *ngIf="state.state == 'questionnaire'"> </div>
<button (click)="dialog.openQuestionnaire(state$)" mat-raised-button class="mat-elevation-z6"> <div *ngIf="!state.is_pending && state.is_credential_pending">
Questionnaire <span>
</button> <mat-progress-spinner diameter="15" mode="indeterminate"></mat-progress-spinner>
</td> SSH Keys pending
<td *ngIf="state.state == 'failed'"> </span>
<button (click)="dialog.openMessage(state)" mat-raised-button class="mat-elevation-z6"> </div>
Failure <div *ngIf="(state.questionnaire | ObjKeys).length > 0 && state.state == 'deployed'"
</button> matTooltip="Change previously submitted answers">
</td> <button (click)="dialog.openQuestionnaire(state$)" mat-icon-button>
<td *ngIf="state.state == 'rejected'"> <mat-icon>edit</mat-icon>
<button (click)="dialog.openMessage(state)" mat-raised-button class="mat-elevation-z6"> </button>
Rejected </div>
</button>
</td>
<td *ngIf="!state.is_pending && state.is_credential_pending">
<span>
<mat-progress-spinner diameter="24" mode="indeterminate"></mat-progress-spinner>
SSH Keys pending
</span>
</td>
<td *ngIf="(state.questionnaire | ObjKeys).length > 0 && state.state == 'deployed'"
matTooltip="Change previously submitted answers">
<button (click)="dialog.openQuestionnaire(state$)" mat-icon-button>
<mat-icon>edit</mat-icon>
</button>
</td> </td>
</ng-container> </ng-container>
<ng-template #noStateItem> <ng-template #noStateItem>
<!-- state column -->
<td> <td>
<span> <span>
<mat-icon mat-icon-button matTooltip="Access to this service was never requested.">call_received</mat-icon> <mat-icon mat-icon-button>call_received</mat-icon>
</span> </span>
{{ lang.printState('not_deployed') }} <h4>{{ lang.printState('not_deployed') }}</h4>
<br>
<small>Access to this service was never requested.</small>
</td> </td>
<!-- actions column -->
<td></td>
</ng-template> </ng-template>
<!-- contacts column -->
<td >
<button mat-raised-button matTooltip="{{ service.contact_email }}" (click)="contactSupport()">
<small>{{ service.contact_description }}</small>
</button>
</td>
...@@ -5,7 +5,13 @@ import { Observable } from 'rxjs'; ...@@ -5,7 +5,13 @@ import { Observable } from 'rxjs';
import { UserService } from '../user.service'; import { UserService } from '../user.service';
import { LanguageService } from '../language.service'; import { LanguageService } from '../language.service';
import { DialogService } from '../dialogues/dialog.service'; import { DialogService } from '../dialogues/dialog.service';
import { Site, Service, DeploymentState } from '../types/types.module'; import { Site, Service, DeploymentState, StateID } from '../types/types.module';
interface Button {
text: string;
tooltip: string;
color: string;
}
@Component({ @Component({
selector: '[deployment-state-tr]', selector: '[deployment-state-tr]',
...@@ -16,9 +22,41 @@ export class StateComponent implements OnInit { ...@@ -16,9 +22,41 @@ export class StateComponent implements OnInit {
@Input() service: Service; @Input() service: Service;
buttons: Record<StateID, Button | undefined> = {
'not_deployed' : undefined,
'deployment_pending': undefined,
'removal_pending': undefined,
'deployed': {
color: 'primary',
text: 'View Credentials',
tooltip: 'View detailed information about the deployed credentials'
},
'questionnaire': {
color: 'warn',
text: 'Submit Data',
tooltip: 'Submit missing data',
},
'failed': {
color: 'warn',
text: 'Show Error',
tooltip: 'Show more details on the error',
},
'rejected': {
color: 'warn',
text: 'Show Message',
tooltip: 'Show Message',
},
};
stateDescriptions: Record<StateID, string>;
// set in subscribe
public button: Button | undefined = undefined;
public stateDescription = '';
public state$: Observable<DeploymentState>; public state$: Observable<DeploymentState>;
public stateTooltip: string;
constructor( constructor(
public userService: UserService, public userService: UserService,
...@@ -27,29 +65,53 @@ export class StateComponent implements OnInit { ...@@ -27,29 +65,53 @@ export class StateComponent implements OnInit {
) { } ) { }
ngOnInit() { ngOnInit() {
this.stateDescriptions = {
'not_deployed': `Your credentials are not deployed for ${ this.service.name }.`,
'deployment_pending': `Waiting for the deployment of your credentials to ${ this.service.site.name }.`,
'removal_pending': `Waiting for the removal of your credentials from ${ this.service.site.name }.`,
'deployed': `
The credentials are deployed for this service ${ this.service.name }.
Click "${ this.buttons['deployed'].text }" see details on your deployed ssh keys and credentials.
`,
'questionnaire': `
More data is needed to deploy your credentials.
Please click "${ this.buttons['questionnaire'].text }" to submit the missing data.
`,
'failed': `
The deployment of your credentials failed, but will be tried again.
Please click "${ this.buttons['failed'].text }" for more details concerning this error.
`,
'rejected': `
The deployment of your credentials was rejected.
Please click "${ this.buttons['rejected'].text }" to show more details.
`,
};
this.state$ = this.userService.subscribeStateFor(this.service); this.state$ = this.userService.subscribeStateFor(this.service);
this.stateTooltip = 'foo'; this.state$.subscribe(
(state: DeploymentState | undefined) => {
if (state !== undefined) {
this.button = this.buttons[state.state];
this.stateDescription = this.stateDescriptions[state.state];
}
}
);
} }
public tooltip(state: DeploymentState): string { public buttonAction(state: DeploymentState): void {
switch (state.state) { switch (state.state) {
case 'not_deployed':
return `The credentials are not deployed for the service ${ this.service.name }.`;
case 'deployed': case 'deployed':
return `The credentials are deployed for the service ${ this.service.name }. Click to see details.`; return this.dialog.openCredentials(this.state$);
case 'deployment_pending':
return `Waiting for the deployment of the credentials to the site ${ this.service.site.name }`;
case 'removal_pending':
return `Waiting for the removal of the credentials from the site ${ this.service.site.name }`;
case 'questionnaire': case 'questionnaire':
return `Site ${ this.service.site.name } needs more data to deploy the keys. Please click to submit the data.`; return this.dialog.openQuestionnaire(this.state$);
case 'failed': case 'failed':
return `Site ${ this.service.site.name } failed to deploy the credentials. The deployment will be retried. Click for details.`; return this.dialog.openMessage(state);
case 'rejected': case 'rejected':
return `Site ${ this.service.site.name } rejected the deployment of the credentials. Click for details.`; return this.dialog.openMessage(state);
default:
return 'Access to this service was never requested.';
} }
} }
public contactSupport() {
window.location.href = `mailto:${ this.service.contact_email }?subject=%5BFEUDAL%5D%20Help%20request%0A`;
}
} }
...@@ -2,6 +2,26 @@ import { NgModule } from '@angular/core'; ...@@ -2,6 +2,26 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
// STATE ENUMS
const states = [
'deployment_pending',
'removal_pending',
'deployed',
'not_deployed',
'questionnaire',
'failed',
'rejected',
] as const;
export type StateID = typeof states[number];
const stateTargets = [
'deployed',
'not_deployed',
] as const;
export type StateTargetID = typeof stateTargets[number];
// auth stuff // auth stuff
export interface IdP { export interface IdP {
id: number; id: number;
...@@ -57,6 +77,8 @@ export interface Service { ...@@ -57,6 +77,8 @@ export interface Service {
site: Site; site: Site;
description: string; description: string;
vos: VO[]; vos: VO[];
contact_email: string;
contact_description: string;
} }
export interface JSONObject { export interface JSONObject {
...@@ -66,8 +88,8 @@ export interface JSONObject { ...@@ -66,8 +88,8 @@ export interface JSONObject {
export interface CredentialState { export interface CredentialState {
credential: SSHKeyRef; credential: SSHKeyRef;
is_pending: boolean; is_pending: boolean;
state: string; state: StateID;
state_target: string; state_target: StateTargetID;
} }
export interface DeploymentState { export interface DeploymentState {
...@@ -81,16 +103,16 @@ export interface DeploymentState { ...@@ -81,16 +103,16 @@ export interface DeploymentState {
questionnaire: JSONObject | undefined; questionnaire: JSONObject | undefined;
service: Service; service: Service;
site: Site; site: Site;
state: string; state: StateID;
state_target: string; state_target: StateTargetID;
} }
export interface Deployment { export interface Deployment {
id: number; id: number;
service?: Service; // only for ServiceDeployment service?: Service; // only for ServiceDeployment
services?: Service[]; // only for VODeployment services?: Service[]; // only for VODeployment
state: string; state: StateID;
state_target: string; state_target: StateTargetID;
states: DeploymentState[]; states: DeploymentState[];
vo?: VO; // only for VODeployment vo?: VO; // only for VODeployment
} }
......
...@@ -221,7 +221,7 @@ export class UserService { ...@@ -221,7 +221,7 @@ export class UserService {
} }
private fetch(): void { private fetch(): void {
this.http.get<State>('/backend/api/state').subscribe( this.http.get<State>('/webpage/state').subscribe(
(state: State) => this.updateState(state), (state: State) => this.updateState(state),
this.handleError(false, 'Error fetching state. Try again later'), this.handleError(false, 'Error fetching state. Try again later'),
); );
...@@ -270,7 +270,7 @@ export class UserService { ...@@ -270,7 +270,7 @@ export class UserService {
} }
private fetchIdPInfo(): void { private fetchIdPInfo(): void {
this.http.get<IdPInfo>('/backend/auth/v1/info').subscribe( this.http.get<IdPInfo>('/auth/info').subscribe(
idpInfo => { idpInfo => {
this.idpInfo.next(idpInfo); this.idpInfo.next(idpInfo);
this.idpInfo.complete(); this.idpInfo.complete();
...@@ -304,27 +304,31 @@ export class UserService { ...@@ -304,27 +304,31 @@ export class UserService {
public login(idp?: IdP): void { public login(idp?: IdP): void {
const redirect = (arg: IdP) => { const redirect = (arg: IdP) => {
window.location.href = '/?idphint=' + encodeURIComponent(arg.issuer_uri); window.location.href = '/?idp=' + encodeURIComponent(arg.issuer_uri);
}; };
// was there an argument?
if (idp !== undefined) { if (idp !== undefined) {
this.prefs.setPreferredIdP(idp);