Skip to content
This repository has been archived by the owner on Jan 24, 2023. It is now read-only.

Add UI for Stratos API Keys #4523

Merged
merged 21 commits into from
Sep 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/frontend/packages/core/src/app.routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ const appRoutes: Routes = [
},
{ path: 'about', loadChildren: () => import('./features/about/about.module').then(m => m.AboutModule) },
{ path: 'user-profile', loadChildren: () => import('./features/user-profile/user-profile.module').then(m => m.UserProfileModule) },
{ path: 'api-keys', loadChildren: () => import('./features/api-keys/api-keys.module').then(m => m.ApiKeysModule) },
{ path: 'events', loadChildren: () => import('./features/event-page/event-page.module').then(m => m.EventPageModule) },
{
path: 'errors/:endpointId',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<div class="key-dialog__loading-wrapper">
<mat-progress-bar class="key-dialog__loading"
[color]="(hasErrored$ | async) && !(isBusy$ | async) ? 'warn' : 'primary'"
[mode]="(isBusy$ | async) ? 'indeterminate' : 'solid'">
</mat-progress-bar>
</div>
<div class="key-dialog">
<div class="key-dialog__title">
<h2 mat-dialog-title>
Create an API Key
</h2>
</div>
<div>
</div>
<div [formGroup]="formGroup">
<mat-form-field>
<input matInput placeholder="Description" formControlName="comment" required>
</mat-form-field>
</div>

<app-dialog-error message="{{hasErrored$ | async}}" [show]="(hasErrored$ | async) && !(isBusy$ | async)">
</app-dialog-error>
<mat-dialog-actions class="key-dialog__actions">
<button [mat-dialog-close]="true" mat-button color="warn" [disabled]="(isBusy$ | async)">Cancel</button>
<button (click)="submit()" [disabled]="(isBusy$ | async) || !formGroup.valid" mat-button
color="primary">Create</button>
</mat-dialog-actions>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.key-dialog {
&__loading {
left: 0;
position: absolute;
right: 0;
top: 0;
&-wrapper {
position: relative;
margin: 0 -24px;
transform: translateY(-24px);
}
}

&__title {
display: flex;
h2 {
flex: 1;
}
}

&__actions {
justify-content: flex-end;
}

mat-form-field {
width: 100%;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MatDialogRef } from '@angular/material/dialog';

import { BaseTestModules } from '../../../../test-framework/core-test.helper';
import { AddApiKeyDialogComponent } from './add-api-key-dialog.component';

describe('AddApiKeyDialogComponent', () => {
let component: AddApiKeyDialogComponent;
let fixture: ComponentFixture<AddApiKeyDialogComponent>;

const mockDialogRef = {
close: () => { }
};

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
...BaseTestModules,
],
declarations: [AddApiKeyDialogComponent],
providers: [
{
provide: MatDialogRef,
useValue: mockDialogRef
}
]
})
.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(AddApiKeyDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Component, OnDestroy } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialogRef } from '@angular/material/dialog';
import { BehaviorSubject, Subscription } from 'rxjs';
import { filter, first, map, pairwise, tap } from 'rxjs/operators';

import { ApiKey } from '../../../../../store/src/apiKey.types';
import { entityCatalog } from '../../../../../store/src/entity-catalog/entity-catalog';
import { RequestInfoState } from '../../../../../store/src/reducers/api-request-reducer/types';
import { stratosEntityCatalog } from '../../../../../store/src/stratos-entity-catalog';
import { NormalizedResponse } from '../../../../../store/src/types/api.types';
import { safeUnsubscribe } from '../../../core/utils.service';

@Component({
selector: 'app-add-api-key-dialog',
templateUrl: './add-api-key-dialog.component.html',
styleUrls: ['./add-api-key-dialog.component.scss']
})
export class AddApiKeyDialogComponent implements OnDestroy {

private hasErrored = new BehaviorSubject(null);
public hasErrored$ = this.hasErrored.asObservable();
private isBusy = new BehaviorSubject(false)
public isBusy$ = this.isBusy.asObservable();

private sub: Subscription;

public formGroup: FormGroup;

constructor(
private fb: FormBuilder,
public dialogRef: MatDialogRef<ApiKey>,
) {
this.formGroup = this.fb.group({
comment: ['', Validators.required],
});
}

ngOnDestroy(): void {
safeUnsubscribe(this.sub);
}

submit() {
this.sub = stratosEntityCatalog.apiKey.api.create<RequestInfoState>(this.formGroup.controls.comment.value).pipe(
tap(() => {
this.isBusy.next(true);
this.hasErrored.next(null);
}),
pairwise(),
filter(([oldR, newR]) => oldR.creating && !newR.creating),
map(([, newR]) => newR),
tap(state => {
if (state.error) {
this.hasErrored.next(`Failed to create key: ${state.message}`);
this.isBusy.next(false);
} else {
const response: NormalizedResponse<ApiKey> = state.response;
const entityKey = entityCatalog.getEntityKey(stratosEntityCatalog.apiKey.actions.create(''));
this.dialogRef.close(response.entities[entityKey][response.result[0]])
}
}),
first()
).subscribe()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<app-page-header>
<h1>API Keys</h1>
<div class="page-header-right">
<button id="stratos-api-key" mat-icon-button (click)="addApiKey()" matTooltip="Create API Key">
<mat-icon>add</mat-icon>
</button>
</div>
</app-page-header>

<div class="keys-page__new" *ngIf="keyDetails$ | async as keyDetails">

<mat-card class="autoscaler-credential__card">
<mat-card-header>
<mat-icon>vpn_key</mat-icon>New API Key
</mat-card-header>
<mat-card-content>
<p>Your API Key has been successfully created. Use the following information to connect to Stratos.</p>
<p><i>Please safely record these details, there is no later way to view them</i></p>
<ul>
<li>Secret: {{keyDetails.secret}}</li>
</ul>
<button (click)="clearKeyDetails()" mat-button color="warn">Close</button>
</mat-card-content>
</mat-card>
</div>

<app-list *ngIf="(hasKeys$ | async) === true"></app-list>
<app-no-content-message *ngIf="(hasKeys$ | async) === false" icon="vpn_key" [firstLine]="'You have no API keys'"
[secondLine]="{
text: ''
}" [toolbarLink]="{
text: 'Create an API key'
}" toolbarAlign="stratos-api-key"></app-no-content-message>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.keys-page {
&__new {
mat-card {
margin-bottom: 24px;
mat-card-header {
align-items: center;
display: flex;
margin-bottom: 15px;
mat-icon {
margin-right: 5px;
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MatDialogRef } from '@angular/material/dialog';

import { BaseTestModules } from '../../../../test-framework/core-test.helper';
import { TabNavService } from '../../../tab-nav.service';
import { ApiKeysPageComponent } from './api-keys-page.component';

describe('ApiKeysPageComponent', () => {
let component: ApiKeysPageComponent;
let fixture: ComponentFixture<ApiKeysPageComponent>;

const mockDialogRef = {
close: () => { }
};

beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
...BaseTestModules,
],
declarations: [ApiKeysPageComponent],
providers: [
{
provide: MatDialogRef,
useValue: mockDialogRef
},
TabNavService
]
})
.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(ApiKeysPageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Component } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Observable, Subject } from 'rxjs';
import { first, map, startWith } from 'rxjs/operators';

import { stratosEntityCatalog } from '../../../../../store/src/stratos-entity-catalog';
import { ApiKeyListConfigService } from '../../../shared/components/list/list-types/apiKeys/apiKey-list-config.service';
import { ListConfig } from '../../../shared/components/list/list.component.types';
import { AddApiKeyDialogComponent } from '../add-api-key-dialog/add-api-key-dialog.component';

@Component({
selector: 'app-api-keys-page',
templateUrl: './api-keys-page.component.html',
styleUrls: ['./api-keys-page.component.scss'],
providers: [{
provide: ListConfig,
useClass: ApiKeyListConfigService,
}]
})
export class ApiKeysPageComponent {

public keyDetails = new Subject<string>();
public keyDetails$ = this.keyDetails.asObservable();
public hasKeys$: Observable<Boolean>;

constructor(
private dialog: MatDialog,
) {
this.hasKeys$ = stratosEntityCatalog.apiKey.store.getPaginationService().entities$.pipe(
map(entities => entities && !!entities.length),
startWith(null),
)
}

addApiKey() {
this.showDialog().pipe(first()).subscribe(key => {
this.keyDetails.next(key);
})
}

clearKeyDetails() {
this.keyDetails.next();
}

private showDialog(): Observable<string> {
return this.dialog.open(AddApiKeyDialogComponent, {
disableClose: true,
}).afterClosed().pipe(
map(newApiKey => {
if (newApiKey && newApiKey.guid) {
stratosEntityCatalog.apiKey.api.getMultiple();
return newApiKey;
}
return null;
})
);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NgModule } from '@angular/core';

import { CoreModule } from '../../core/core.module';
import { SharedModule } from '../../shared/shared.module';
import { ApiKeysPageComponent } from './api-keys-page/api-keys-page.component';
import { ApiKeysRoutingModule } from './api-keys.routing';
import { AddApiKeyDialogComponent } from './add-api-key-dialog/add-api-key-dialog.component';


@NgModule({
imports: [
CoreModule,
SharedModule,
ApiKeysRoutingModule,
],
declarations: [
ApiKeysPageComponent,
AddApiKeyDialogComponent
]
})
export class ApiKeysModule { }

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { ApiKeysPageComponent } from './api-keys-page/api-keys-page.component';

const apiKeys: Routes = [
{
path: '',
component: ApiKeysPageComponent
},
];

@NgModule({
imports: [
RouterModule.forChild(apiKeys),
]
})
export class ApiKeysRoutingModule { }
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ <h1>Endpoints</h1>
[routerLink]="'/endpoints/new/'" matTooltip="Register Endpoint">
<mat-icon>add</mat-icon>
</button>
<button id="stratos-add-endpoint" *appUserPermission="canRegisterEndpoint" mat-icon-button
<button id="stratos-restore-backup" *appUserPermission="canRegisterEndpoint" mat-icon-button
[routerLink]="'/endpoints/backup-restore'" matTooltip="Backup/Restore Endpoints">
<mat-icon>settings_backup_restore</mat-icon>
</button>
Expand Down
Loading