Change UI for direct messaging and a whole slew of fixes

These are UI changes
1.  Added a constructor for websocket message
2.  Fixed the airshipui-ui to be airshipui
3.  Fixed the refresh not displaying a page
4.  Renamed the angular components to match the go backend
5.  Change the websocket service from multicast to direct messaging
6.  Collapsed the notification service into the websocket service

Change-Id: If8cd5d7006934b9eb59e1ea702d5158c93bfacfe
This commit is contained in:
Schiefelbein, Andrew 2020-08-05 14:41:28 -05:00
parent 0011d08420
commit a7c518e4d6
47 changed files with 539 additions and 549 deletions

View File

@ -3,7 +3,7 @@
"version": 1,
"newProjectRoot": "projects",
"projects": {
"airshipui-ui": {
"airshipui": {
"projectType": "application",
"schematics": {},
"root": "",
@ -13,7 +13,7 @@
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/airshipui-ui",
"outputPath": "dist/airshipui",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
@ -64,18 +64,18 @@
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "airshipui-ui:build"
"browserTarget": "airshipui:build"
},
"configurations": {
"production": {
"browserTarget": "airshipui-ui:build:production"
"browserTarget": "airshipui:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "airshipui-ui:build"
"browserTarget": "airshipui:build"
}
},
"test": {
@ -112,15 +112,15 @@
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "airshipui-ui:serve"
"devServerTarget": "airshipui:serve"
},
"configurations": {
"production": {
"devServerTarget": "airshipui-ui:serve:production"
"devServerTarget": "airshipui:serve:production"
}
}
}
}
}},
"defaultProject": "airshipui-ui"
"defaultProject": "airshipui"
}

View File

@ -1,15 +0,0 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-airship',
templateUrl: './airship.component.html',
styleUrls: ['./airship.component.css']
})
export class AirshipComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@ -1,26 +0,0 @@
import { Component, OnInit } from '@angular/core';
import {WebsocketMessage} from '../../../services/websocket/models/websocket-message/websocket-message';
import {WebsocketService} from '../../../services/websocket/websocket.service';
@Component({
selector: 'app-bare-metal',
templateUrl: './bare-metal.component.html',
styleUrls: ['./bare-metal.component.css']
})
export class BareMetalComponent implements OnInit {
private message: WebsocketMessage;
constructor(private websocketService: WebsocketService) {
}
ngOnInit(): void { }
generateIso(): void {
this.message = new WebsocketMessage();
this.message.type = 'airshipctl';
this.message.component = 'baremetal';
this.message.subComponent = 'generateISO';
this.websocketService.sendMessage(this.message);
}
}

View File

@ -1,44 +0,0 @@
<div id="DocContainer">
<div id="SourceTab" class="tabcontent">
<div id="ViewType">
<mat-button-toggle-group name="viewSelector" aria-label="View Selector">
<mat-button-toggle value="source" (click)="getSource()" checked="true">Source</mat-button-toggle>
<mat-button-toggle value="rendered" (click)="getRendered()">Rendered</mat-button-toggle>
</mat-button-toggle-group>
</div>
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl" class="kustom-tree">
<mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle>
<li class="mat-tree-node">
<button mat-icon-button disabled></button>
<button (click)="getYaml(node.id)">{{node.name}}</button>
</li>
</mat-tree-node>
<mat-nested-tree-node *matTreeNodeDef="let node; when: hasChild">
<li>
<div class="mat-tree-node">
<button mat-icon-button matTreeNodeToggle
[attr.aria-label]="'toggle ' + node.name">
<mat-icon class="mat-icon-rtl-mirror">
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
</mat-icon>
</button>
{{node.name}}
</div>
<ul [class.kustom-tree-invisible]="!treeControl.isExpanded(node)">
<ng-container matTreeNodeOutlet></ng-container>
</ul>
</li>
</mat-nested-tree-node>
</mat-tree>
</div>
<div id="EditorDiv" class="tabcontent">
<div id="EditorHeader">
<div id="EditorTitle"><h5 id="doc-name">{{editorTitle}}</h5></div>
<div *ngIf="!hideButtons" id="EditorButtons">
<button type="button" class="editor-btn" id="SaveYamlBtn" (click)="saveYaml()" [disabled]="saveBtnDisabled">Save</button>
<button type="button" class="editor-btn" (click)="closeEditor()">Close</button>
</div>
</div>
<ngx-monaco-editor *ngIf="code !== undefined && code !== null" [options]="editorOptions" [(ngModel)]="code" (onInit)="onInit($event)"></ngx-monaco-editor>
</div>
</div>

View File

@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DocumentOverviewComponent } from './document-overview.component';
describe('DocumentOverviewComponent', () => {
let component: DocumentOverviewComponent;
let fixture: ComponentFixture<DocumentOverviewComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DocumentOverviewComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DocumentOverviewComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,124 +0,0 @@
import { Component, OnInit } from '@angular/core';
import {WebsocketService} from '../../../../services/websocket/websocket.service';
import {WebsocketMessage} from '../../../../services/websocket/models/websocket-message/websocket-message';
import {KustomNode} from '../../../../app/airship/document/document-overview/kustom-node';
import {NestedTreeControl} from '@angular/cdk/tree';
import {MatTreeNestedDataSource} from '@angular/material/tree';
@Component({
selector: 'app-document-overview',
templateUrl: './document-overview.component.html',
styleUrls: ['./document-overview.component.css']
})
export class DocumentOverviewComponent implements OnInit {
obj: KustomNode[] = [];
currentDocId: string;
saveBtnDisabled: boolean = true;
hideButtons: boolean = true;
isRendered: boolean = false;
editorOptions = {language: 'yaml', automaticLayout: true, value: ''};
code: string;
editorTitle: string;
onInit(editor) {
editor.onDidChangeModelContent(() => {
this.saveBtnDisabled = false;
});
}
treeControl = new NestedTreeControl<KustomNode>(node => node.children);
dataSource = new MatTreeNestedDataSource<KustomNode>();
constructor(private websocketService: WebsocketService) {}
hasChild = (_: number, node: KustomNode) => !!node.children && node.children.length > 0;
ngOnInit(): void {
this.websocketService.subject.subscribe(message => {
if (message.type === 'airshipctl' && message.component === 'document') {
switch (message.subComponent) {
case 'getDefaults':
Object.assign(this.obj, message.data);
this.dataSource.data = this.obj;
break;
case 'getSource':
this.closeEditor();
Object.assign(this.obj, message.data);
this.dataSource.data = this.obj;
break;
case 'getRendered':
this.closeEditor();
Object.assign(this.obj, message.data);
this.dataSource.data = this.obj;
break;
case 'getYaml':
this.changeEditorContents((message.yaml));
this.editorTitle = message.name;
this.currentDocId = message.message;
if (!this.isRendered) {
this.hideButtons = false;
} else {
this.hideButtons = true;
}
break;
case 'yamlWrite':
this.changeEditorContents((message.yaml));
this.editorTitle = message.name;
this.currentDocId = message.message;
break;
}
}
});
const websocketMessage = this.constructDocumentWsMessage("getDefaults");
this.websocketService.sendMessage(websocketMessage);
}
getYaml(id: string): void {
this.code = null;
const websocketMessage = this.constructDocumentWsMessage("getYaml");
websocketMessage.message = id;
this.websocketService.sendMessage(websocketMessage);
}
changeEditorContents(yaml: string): void {
this.code = atob(yaml);
}
saveYaml(): void {
const websocketMessage = this.constructDocumentWsMessage("yamlWrite");
websocketMessage.message = this.currentDocId;
websocketMessage.name = this.editorTitle;
websocketMessage.yaml = btoa(this.code);
this.websocketService.sendMessage(websocketMessage);
}
getSource(): void {
this.isRendered = false;
const websocketMessage = this.constructDocumentWsMessage("getSource");
this.websocketService.sendMessage(websocketMessage);
}
getRendered(): void {
this.isRendered = true;
const websocketMessage = this.constructDocumentWsMessage("getRendered");
this.websocketService.sendMessage(websocketMessage);
}
constructDocumentWsMessage(subComponent: string): WebsocketMessage {
const w = new WebsocketMessage();
w.type = "airshipctl";
w.component = "document";
w.subComponent = subComponent;
return w;
}
closeEditor(): void {
this.code = null;
this.editorTitle = "";
this.hideButtons = true;
}
}

View File

@ -1,3 +0,0 @@
<button type="button" class="btn btn-info" id="DocPullBtn" (click)="documentPull()" style="width: 150px;">Document Pull</button>
<p>Response to Pull: {{obby}}</p>

View File

@ -1,25 +0,0 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DocumentPullComponent } from './document-pull.component';
describe('DocumentPullComponent', () => {
let component: DocumentPullComponent;
let fixture: ComponentFixture<DocumentPullComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DocumentPullComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DocumentPullComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,35 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { WebsocketService } from '../../../../services/websocket/websocket.service';
import { WebsocketMessage } from '../../../../services/websocket/models/websocket-message/websocket-message';
@Component({
selector: 'app-document-pull',
templateUrl: './document-pull.component.html',
styleUrls: ['./document-pull.component.css']
})
export class DocumentPullComponent implements OnInit {
obby: string;
constructor(private websocketService: WebsocketService) {
}
ngOnInit(): void {
this.websocketService.subject.subscribe(message => {
if (message.type === 'airshipctl' && message.component === 'document' && message.subComponent === 'docPull') {
this.obby = JSON.stringify(message);
}
}
);
}
documentPull(): void {
const websocketMessage = new WebsocketMessage();
websocketMessage.type = 'airshipctl';
websocketMessage.component = 'document';
websocketMessage.subComponent = 'docPull';
this.websocketService.sendMessage(websocketMessage);
}
}

View File

@ -1,5 +0,0 @@
<nav mat-tab-nav-bar>
<a mat-tab-link routerLink="overview" (click)="activeLink = 'overview'" [active]="activeLink == 'overview'">Document Overview</a>
<a mat-tab-link routerLink="pull" (click)="activeLink = 'pull'" [active]="activeLink == 'pull'">Document Pull</a>
</nav>
<router-outlet></router-outlet>

View File

@ -1,16 +0,0 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-document',
templateUrl: './document.component.html',
styleUrls: ['./document.component.css']
})
export class DocumentComponent implements OnInit {
activeLink = 'overview';
constructor() { }
ngOnInit(): void {
}
}

View File

@ -2,32 +2,21 @@ import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { DashboardsComponent } from './dashboards/dashboards.component';
import { AirshipComponent } from './airship/airship.component';
import { BareMetalComponent } from './airship/bare-metal/bare-metal.component';
import { DocumentComponent } from './airship/document/document.component';
import { DocumentOverviewComponent } from './airship/document/document-overview/document-overview.component';
import { DocumentPullComponent } from './airship/document/document-pull/document-pull.component';
import { CTLComponent } from './ctl/ctl.component';
import { BareMetalComponent } from './ctl/baremetal/baremetal.component';
import { DocumentComponent } from './ctl/document/document.component';
const routes: Routes = [
{
path: 'airship',
component: AirshipComponent,
path: 'ctl',
component: CTLComponent,
children: [
{
path: 'bare-metal',
path: 'baremetal',
component: BareMetalComponent
}, {
path: 'documents',
component: DocumentComponent,
children: [
{
path: 'overview',
component: DocumentOverviewComponent
}, {
path: 'pull',
component: DocumentPullComponent
}]
component: DocumentComponent
}]
}, {
path: 'dashboard',

View File

@ -2,16 +2,19 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
import { NavInterface } from './models/nav.interface';
import { environment } from '../environments/environment';
import { IconService } from '../services/icon/icon.service';
import { NotificationService } from '../services/notification/notification.service';
import {WebsocketService} from '../services/websocket/websocket.service';
import {Dashboard} from '../services/websocket/models/websocket-message/dashboard/dashboard';
import { WebsocketService } from '../services/websocket/websocket.service';
import { WSReceiver } from '../services/websocket/websocket.models';
import { Dashboard } from '../services/websocket/models/websocket-message/dashboard/dashboard';
import { WebsocketMessage } from 'src/services/websocket/models/websocket-message/websocket-message';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnDestroy, OnInit {
export class AppComponent implements OnInit, WSReceiver {
type: string = "ui";
component: string = "any";
currentYear: number;
version: string;
@ -23,11 +26,11 @@ export class AppComponent implements OnDestroy, OnInit {
children: [
{
displayName: 'Bare Metal',
route: 'airship/bare-metal',
route: 'ctl/baremetal',
iconName: 'server'
}, {
displayName: 'Documents',
route: 'airship/documents/overview',
route: 'ctl/documents',
iconName: 'doc'
}]
}, {
@ -36,18 +39,23 @@ export class AppComponent implements OnDestroy, OnInit {
}];
constructor(private iconService: IconService,
private notificationService: NotificationService,
private websocketService: WebsocketService) {
this.currentYear = new Date().getFullYear();
this.version = environment.version;
this.websocketService.subject.subscribe(message => {
if (message.type === 'airshipui' && message.component === 'initialize' && message.dashboards !== undefined) {
this.updateDashboards(message.dashboards);
}
});
this.websocketService.registerFunctions(this);
}
ngOnDestroy(): void {
async receiver(message: WebsocketMessage): Promise<void> {
if (message.hasOwnProperty("error")) {
this.websocketService.printIfToast(message);
} else {
if (message.hasOwnProperty("dashboards")) {
this.updateDashboards(message.dashboards);
} else {
// TODO (aschiefe): determine what should be notifications and what should be 86ed
console.log("Message received in app: ", message);
}
}
}
ngOnInit(): void {

View File

@ -14,15 +14,13 @@ import { MatTableModule } from '@angular/material/table';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatExpansionModule } from '@angular/material/expansion';
import { RouterModule } from '@angular/router';
import { AirshipComponent } from './airship/airship.component';
import { CTLComponent } from './ctl/ctl.component';
import { DashboardsComponent } from './dashboards/dashboards.component';
import { HomeComponent } from './home/home.component';
import { BareMetalComponent } from './airship/bare-metal/bare-metal.component';
import { DocumentComponent } from './airship/document/document.component';
import { BareMetalComponent } from './ctl/baremetal/baremetal.component';
import { DocumentComponent } from './ctl/document/document.component';
import { HttpClientModule } from '@angular/common/http';
import { FlexLayoutModule } from '@angular/flex-layout';
import { DocumentOverviewComponent } from './airship/document/document-overview/document-overview.component';
import { DocumentPullComponent } from './airship/document/document-pull/document-pull.component';
import { MatTabsModule } from '@angular/material/tabs';
import { WebsocketService } from '../services/websocket/websocket.service';
import { ToastrModule } from 'ngx-toastr';
@ -34,13 +32,11 @@ import {MatButtonToggleModule} from '@angular/material/button-toggle';
@NgModule({
declarations: [
AppComponent,
AirshipComponent,
CTLComponent,
DashboardsComponent,
HomeComponent,
BareMetalComponent,
DocumentComponent,
DocumentOverviewComponent,
DocumentPullComponent,
],
imports: [
AppRoutingModule,

View File

@ -1,6 +1,6 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { BareMetalComponent } from './bare-metal.component';
import { BareMetalComponent } from './baremetal.component';
describe('BareMetalComponent', () => {
let component: BareMetalComponent;

View File

@ -0,0 +1,33 @@
import { Component, OnInit } from '@angular/core';
import {WebsocketMessage} from '../../../services/websocket/models/websocket-message/websocket-message';
import {WebsocketService} from '../../../services/websocket/websocket.service';
import { WSReceiver } from '../../../services/websocket//websocket.models';
@Component({
selector: 'app-bare-metal',
templateUrl: './baremetal.component.html',
styleUrls: ['./baremetal.component.css']
})
export class BareMetalComponent implements WSReceiver {
// TODO (aschiefe): extract these strings to constants
type: string = "ctl";
component: string = "baremetal";
constructor(private websocketService: WebsocketService) {
this.websocketService.registerFunctions(this);
}
async receiver(message: WebsocketMessage): Promise<void> {
if (message.hasOwnProperty("error")) {
this.websocketService.printIfToast(message);
} else {
// TODO (aschiefe): determine what should be notifications and what should be 86ed
console.log("Message received in baremetal: ", message);
}
}
generateIso(): void {
this.websocketService.sendMessage(new WebsocketMessage(this.type, this.component, "generateISO"));
}
}

View File

@ -0,0 +1,10 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-airship',
templateUrl: './ctl.component.html',
styleUrls: ['./ctl.component.css']
})
export class CTLComponent {
}

View File

@ -1,20 +1,20 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AirshipComponent } from './airship.component';
import { CTLComponent } from './ctl.component';
describe('AirshipComponent', () => {
let component: AirshipComponent;
let fixture: ComponentFixture<AirshipComponent>;
describe('CTLComponent', () => {
let component: CTLComponent;
let fixture: ComponentFixture<CTLComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AirshipComponent ]
declarations: [ CTLComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AirshipComponent);
fixture = TestBed.createComponent(CTLComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -1,54 +1,54 @@
@import '~material-design-icons/iconfont/material-icons.css';
#DocContainer {
display: flex;
height: 75vh;
}
#SourceTab {
width: 40%;
overflow-y: auto;
}
button {
border: none;
background: none;
}
#ViewType {
padding: 5px;
}
.kustom-tree-invisible {
display: none;
}
.kustom-tree ul,
.kustom-tree li {
margin-top: 0;
margin-bottom: 0;
list-style-type: none;
}
#EditorDiv {
height: 100%;
width: 60%;
padding-left: 5px
}
#EditorHeader {
display: flex;
}
#EditorButtons {
margin-left: auto;
}
.editor-btn {
padding: 5px;
}
ngx-monaco-editor {
height: 100%;
width: 100%;
@import '~material-design-icons/iconfont/material-icons.css';
#DocContainer {
display: flex;
height: 75vh;
}
#SourceTab {
width: 40%;
overflow-y: auto;
}
button {
border: none;
background: none;
}
#ViewType {
padding: 5px;
}
.kustom-tree-invisible {
display: none;
}
.kustom-tree ul,
.kustom-tree li {
margin-top: 0;
margin-bottom: 0;
list-style-type: none;
}
#EditorDiv {
height: 100%;
width: 60%;
padding-left: 5px
}
#EditorHeader {
display: flex;
}
#EditorButtons {
margin-left: auto;
}
.editor-btn {
padding: 5px;
}
ngx-monaco-editor {
height: 100%;
width: 100%;
}

View File

@ -0,0 +1,53 @@
<mat-tab-group>
<mat-tab label="Document Overview">
<div id="DocContainer">
<div id="SourceTab" class="tabcontent">
<div id="ViewType">
<mat-button-toggle-group name="viewSelector" aria-label="View Selector">
<mat-button-toggle value="source" (click)="getSource()" checked="true">Source</mat-button-toggle>
<mat-button-toggle value="rendered" (click)="getRendered()">Rendered</mat-button-toggle>
</mat-button-toggle-group>
</div>
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl" class="kustom-tree">
<mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle>
<li class="mat-tree-node">
<button mat-icon-button disabled></button>
<button (click)="getYaml(node.id)">{{node.name}}</button>
</li>
</mat-tree-node>
<mat-nested-tree-node *matTreeNodeDef="let node; when: hasChild">
<li>
<div class="mat-tree-node">
<button mat-icon-button matTreeNodeToggle
[attr.aria-label]="'toggle ' + node.name">
<mat-icon class="mat-icon-rtl-mirror">
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
</mat-icon>
</button>
{{node.name}}
</div>
<ul [class.kustom-tree-invisible]="!treeControl.isExpanded(node)">
<ng-container matTreeNodeOutlet></ng-container>
</ul>
</li>
</mat-nested-tree-node>
</mat-tree>
</div>
<div id="EditorDiv" class="tabcontent">
<div id="EditorHeader">
<div id="EditorTitle"><h5 id="doc-name">{{editorTitle}}</h5></div>
<div *ngIf="!hideButtons" id="EditorButtons">
<button type="button" class="editor-btn" id="SaveYamlBtn" (click)="saveYaml()" [disabled]="saveBtnDisabled">Save</button>
<button type="button" class="editor-btn" (click)="closeEditor()">Close</button>
</div>
</div>
<ngx-monaco-editor *ngIf="code !== undefined && code !== null" [options]="editorOptions" [(ngModel)]="code" (onInit)="onInit($event)"></ngx-monaco-editor>
</div>
</div>
</mat-tab>
<mat-tab label="Document Pull">
<br>
<button type="button" class="btn btn-info" id="DocPullBtn" (click)="documentPull()" style="width: 150px;">Document Pull</button>
<p>Response to Pull: {{obby}}</p>
</mat-tab>
</mat-tab-group>

View File

@ -0,0 +1,138 @@
import { Component, OnInit } from '@angular/core';
import {WebsocketService} from '../../../services/websocket/websocket.service';
import { WSReceiver } from '../../../services/websocket/websocket.models';
import {WebsocketMessage} from '../../../services/websocket/models/websocket-message/websocket-message';
import {KustomNode} from './kustom-node';
import {NestedTreeControl} from '@angular/cdk/tree';
import {MatTreeNestedDataSource} from '@angular/material/tree';
@Component({
selector: 'app-document',
templateUrl: './document.component.html',
styleUrls: ['./document.component.css']
})
export class DocumentComponent implements WSReceiver {
obby: string;
type: string = 'ctl';
component: string = 'document';
activeLink = 'overview';
obj: KustomNode[] = [];
currentDocId: string;
saveBtnDisabled: boolean = true;
hideButtons: boolean = true;
isRendered: boolean = false;
editorOptions = {language: 'yaml', automaticLayout: true, value: ''};
code: string;
editorTitle: string;
onInit(editor) {
editor.onDidChangeModelContent(() => {
this.saveBtnDisabled = false;
});
}
treeControl = new NestedTreeControl<KustomNode>(node => node.children);
dataSource = new MatTreeNestedDataSource<KustomNode>();
constructor(private websocketService: WebsocketService) {
this.websocketService.registerFunctions(this);
this.getSource(); // load the source first
}
hasChild = (_: number, node: KustomNode) => !!node.children && node.children.length > 0;
public async receiver(message: WebsocketMessage): Promise<void> {
if (message.hasOwnProperty("error")) {
this.websocketService.printIfToast(message);
} else {
switch (message.subComponent) {
case 'getDefaults':
Object.assign(this.obj, message.data);
this.dataSource.data = this.obj;
break;
case 'getSource':
this.closeEditor();
Object.assign(this.obj, message.data);
this.dataSource.data = this.obj;
break;
case 'getRendered':
this.closeEditor();
Object.assign(this.obj, message.data);
this.dataSource.data = this.obj;
break;
case 'getYaml':
this.changeEditorContents((message.yaml));
this.editorTitle = message.name;
this.currentDocId = message.message;
if (!this.isRendered) {
this.hideButtons = false;
} else {
this.hideButtons = true;
}
break;
case 'yamlWrite':
this.changeEditorContents((message.yaml));
this.editorTitle = message.name;
this.currentDocId = message.message;
break;
case 'docPull':
this.obby = "Message pull was a " + message.message;
break;
default:
console.log("Document message sub component not handled: ", message);
break;
}
}
}
getYaml(id: string): void {
this.code = null;
const websocketMessage = this.constructDocumentWsMessage("getYaml");
websocketMessage.message = id;
this.websocketService.sendMessage(websocketMessage);
}
changeEditorContents(yaml: string): void {
this.code = atob(yaml);
}
saveYaml(): void {
const websocketMessage = this.constructDocumentWsMessage("yamlWrite");
websocketMessage.message = this.currentDocId;
websocketMessage.name = this.editorTitle;
websocketMessage.yaml = btoa(this.code);
this.websocketService.sendMessage(websocketMessage);
}
getSource(): void {
this.isRendered = false;
const websocketMessage = this.constructDocumentWsMessage("getSource");
this.websocketService.sendMessage(websocketMessage);
}
getRendered(): void {
this.isRendered = true;
const websocketMessage = this.constructDocumentWsMessage("getRendered");
this.websocketService.sendMessage(websocketMessage);
}
constructDocumentWsMessage(subComponent: string): WebsocketMessage {
return new WebsocketMessage(this.type, this.component, subComponent);
}
closeEditor(): void {
this.code = null;
this.editorTitle = "";
this.hideButtons = true;
}
documentPull(): void {
this.websocketService.sendMessage(new WebsocketMessage(this.type, this.component, "docPull"));
}
}

View File

@ -5,11 +5,6 @@ import { Component, OnInit } from '@angular/core';
templateUrl: './dashboards.component.html',
styleUrls: ['./dashboards.component.css']
})
export class DashboardsComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
export class DashboardsComponent {
}

View File

@ -1,16 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { NotificationService } from './notification.service';
describe('NotificationService', () => {
let service: NotificationService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(NotificationService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -1,24 +0,0 @@
import { Injectable } from '@angular/core';
import { ToastrService } from 'ngx-toastr';
import { WebsocketService } from '../websocket/websocket.service';
import { WebsocketMessage } from '../websocket/models/websocket-message/websocket-message';
@Injectable({
providedIn: 'root'
})
export class NotificationService {
constructor(private toastrService: ToastrService,
private websocketService: WebsocketService) {
this.websocketService.subject.subscribe(message => {
this.printIfToast(message);
}
);
}
printIfToast(message: WebsocketMessage): void {
if (message.error !== undefined && message.error !== null) {
this.toastrService.error(message.error);
}
}
}

View File

@ -14,4 +14,12 @@ export class WebsocketMessage {
message: string;
data: JSON;
yaml: string;
// this constructor looks like this in case anyone decides they want just a raw message with no data predefined
// or an easy way to specify the defaults
constructor (type?: string | undefined, component?: string | undefined, subComponent?: string | undefined) {
this.type = type;
this.component = component;
this.subComponent = subComponent;
}
}

View File

@ -0,0 +1,10 @@
import { WebsocketMessage } from './models/websocket-message/websocket-message';
export interface WSReceiver {
// the holy trinity of the websocket messages, a triumvirate if you will, which is how all are routed
type: string;
component: string;
// This is the method which will need to be implemented in the component to handle the messages
receiver(message: WebsocketMessage): Promise<void>;
}

View File

@ -13,4 +13,4 @@ describe('WebsocketService', () => {
it('should be created', () => {
expect(service).toBeTruthy();
});
});
});

View File

@ -1,45 +1,56 @@
import { Injectable } from '@angular/core';
import { Injectable, OnDestroy } from '@angular/core';
import { WebsocketMessage } from './models/websocket-message/websocket-message';
import { Subject } from 'rxjs';
import {Dashboard} from './models/websocket-message/dashboard/dashboard';
import {Executable} from './models/websocket-message/dashboard/executable/executable';
import { WSReceiver } from './websocket.models';
import { ToastrService } from 'ngx-toastr';
import 'reflect-metadata';
@Injectable({
providedIn: 'root'
})
export class WebsocketService {
public subject = new Subject<WebsocketMessage>();
export class WebsocketService implements OnDestroy {
private ws: WebSocket;
private timeout: number;
// functionMap is how we know where to send the direct messages
// the structure of this map is: type -> component -> receiver
private functionMap = new Map<string, Map<string,WSReceiver>>();
// messageToObject unmarshalls the incoming message into a WebsocketMessage object
private static messageToObject(incomingMessage: string): WebsocketMessage {
let json = JSON.parse(incomingMessage);
let obj = new WebsocketMessage();
Object.assign(obj, json);
let wsm = new WebsocketMessage();
Object.assign(wsm, json);
return obj;
return wsm;
}
constructor() {
// when the WebsocketService is created the toast message is initialized and a websocket is registered
constructor(private toastrService: ToastrService) {
this.register();
}
public sendMessage(message: WebsocketMessage): void {
// catch the page destroy and shut down the websocket connection normally
ngOnDestroy(): void {
this.ws.close();
}
// sendMessage will relay a WebsocketMessage to the go backend
public async sendMessage(message: WebsocketMessage): Promise<void> {
message.timestamp = new Date().getTime();
this.ws.send(JSON.stringify(message));
}
// register initializes the websocket communication with the go backend
private register(): void {
if (this.ws !== undefined && this.ws !== null) {
this.ws.close();
this.ws = null;
}
this.ws = new WebSocket('ws://localhost:8080/ws');
this.ws.onmessage = (event) => {
this.subject.next(WebsocketService.messageToObject(event.data));
this.messageHandler(WebsocketService.messageToObject(event.data));
};
this.ws.onerror = (event) => {
@ -48,8 +59,6 @@ export class WebsocketService {
this.ws.onopen = () => {
console.log('Websocket established');
const json = { type: 'airshipui', component: 'initialize' };
this.ws.send(JSON.stringify(json));
// start up the keepalive so the websocket-message stays open
this.keepAlive();
};
@ -113,14 +122,60 @@ export class WebsocketService {
this.ws = null;
}
// Takes the WebsocketMessage and iterates through the function map to send a directed message when it shows up
private async messageHandler(message: WebsocketMessage): Promise<void> {
switch (message.type) {
case 'alert': this.toastrService.warning(message.message); break; // TODO (aschiefe): improve alert handling
default: if (this.functionMap.hasOwnProperty(message.type)) {
if (this.functionMap[message.type].hasOwnProperty(message.component)){
this.functionMap[message.type][message.component].receiver(message);
} else {
// special case where we want to handle all top level messages at a specific component
if (this.functionMap[message.type].hasOwnProperty("any")) {
this.functionMap[message.type]["any"].receiver(message);
} else {
this.printIfToast(message);
}
}
} else {
this.toastrService.info(message.message);
}
break;
}
}
// websockets time out after 5 minutes of inactivity, this keeps the backend engaged so it doesn't time
private keepAlive(): void {
if (this.ws !== undefined && this.ws !== null && this.ws.readyState !== this.ws.CLOSED) {
// clear the previously set timeout
window.clearTimeout(this.timeout);
window.clearInterval(this.timeout);
const json = { type: 'airshipui', component: 'keepalive' };
const json = { type: 'ui', component: 'keepalive' };
this.ws.send(JSON.stringify(json));
this.timeout = window.setTimeout(this.keepAlive, 60000);
}
}
// registerFunctions is a is called out of the target's constructor so it can auto populate the function map
public registerFunctions(target: WSReceiver): void {
let type = target.type;
let component = target.component;
if (this.functionMap.hasOwnProperty(type)) {
this.functionMap[type][component] = target;
} else {
let components = new Map<string,WSReceiver>();
components[component] = target;
this.functionMap[type] = components;
}
}
// printIfToast puts up the toast popup message on the UI
printIfToast(message: WebsocketMessage): void {
if (message.error !== undefined && message.error !== null) {
this.toastrService.error(message.error);
} else {
console.log(message);
this.toastrService.info(message.message);
}
}
}

View File

@ -67,9 +67,9 @@ type WsSubComponentType string
// constants related to specific request/component/subcomponent types for WsRequests
const (
AirshipCTL WsRequestType = "airshipctl"
AirshipUI WsRequestType = "airshipui"
Alert WsRequestType = "alert"
CTL WsRequestType = "ctl"
UI WsRequestType = "ui"
Alert WsRequestType = "alert"
Authcomplete WsComponentType = "authcomplete"
Error WsComponentType = "danger" // Error corresponds to a red alert message if used as an alert

View File

@ -25,7 +25,7 @@ import (
// This will wait for the sub component to complete before responding. The assumption is this is an async request
func HandleBaremetalRequest(request configs.WsMessage) configs.WsMessage {
response := configs.WsMessage{
Type: configs.AirshipCTL,
Type: configs.CTL,
Component: configs.Baremetal,
SubComponent: request.SubComponent,
}

View File

@ -26,7 +26,7 @@ func TestHandleDefaultBaremetalRequest(t *testing.T) {
utiltest.InitConfig(t)
request := configs.WsMessage{
Type: configs.AirshipCTL,
Type: configs.CTL,
Component: configs.Baremetal,
SubComponent: configs.GetDefaults,
}
@ -34,7 +34,7 @@ func TestHandleDefaultBaremetalRequest(t *testing.T) {
response := HandleBaremetalRequest(request)
expected := configs.WsMessage{
Type: configs.AirshipCTL,
Type: configs.CTL,
Component: configs.Baremetal,
SubComponent: configs.GetDefaults,
}
@ -46,7 +46,7 @@ func TestHandleDefaultBaremetalRequest(t *testing.T) {
func TestHandleUnknownBaremetalSubComponent(t *testing.T) {
request := configs.WsMessage{
Type: configs.AirshipCTL,
Type: configs.CTL,
Component: configs.Baremetal,
SubComponent: "fake_subcomponent",
}
@ -54,7 +54,7 @@ func TestHandleUnknownBaremetalSubComponent(t *testing.T) {
response := HandleBaremetalRequest(request)
expected := configs.WsMessage{
Type: configs.AirshipCTL,
Type: configs.CTL,
Component: configs.Baremetal,
SubComponent: "fake_subcomponent",
Error: "Subcomponent fake_subcomponent not found",

View File

@ -37,7 +37,7 @@ var (
// HandleDocumentRequest will flop between requests so we don't have to have them all mapped as function calls
func HandleDocumentRequest(request configs.WsMessage) configs.WsMessage {
response := configs.WsMessage{
Type: configs.AirshipCTL,
Type: configs.CTL,
Component: configs.Document,
SubComponent: request.SubComponent,
}

View File

@ -23,7 +23,7 @@ import (
func TestHandleUnknownDocumentSubComponent(t *testing.T) {
request := configs.WsMessage{
Type: configs.AirshipCTL,
Type: configs.CTL,
Component: configs.Document,
SubComponent: "fake_subcomponent",
}
@ -31,7 +31,7 @@ func TestHandleUnknownDocumentSubComponent(t *testing.T) {
response := HandleDocumentRequest(request)
expected := configs.WsMessage{
Type: configs.AirshipCTL,
Type: configs.CTL,
Component: configs.Document,
SubComponent: "fake_subcomponent",
Error: "Subcomponent fake_subcomponent not found",

View File

@ -45,7 +45,7 @@ func SendAlert(lvl configs.WsComponentType, msg string, fade bool) {
}
func sendAlertMessage(a configs.WsMessage) {
if err := ws.WriteJSON(a); err != nil {
if err := WebSocketSend(a); err != nil {
onError(err)
}
}

View File

@ -30,8 +30,7 @@ func TestSendAlert(t *testing.T) {
// construct and send alert from server to client
SendAlert(configs.Error, "Test Alert", true)
var response configs.WsMessage
err = client.ReadJSON(&response)
response, err := MessageReader(client)
require.NoError(t, err)
expected := configs.WsMessage{

View File

@ -29,23 +29,29 @@ import (
var isAuthenticated bool
const (
clientPath = "client/dist/airshipui-ui"
staticContent = "client/dist/airshipui"
)
// test if path and file exists, if it does send a page, else 404 for you
func serveFile(w http.ResponseWriter, r *http.Request) {
filePath, filePathErr := utilfile.FilePath(clientPath, r.URL.Path)
filePath, filePathErr := utilfile.FilePath(staticContent, r.URL.Path)
if filePathErr != nil {
utilhttp.HandleErr(w, errors.WithStack(filePathErr), http.StatusInternalServerError)
return
}
fileExists, fileExistsErr := utilfile.Exists(filePath)
if fileExistsErr != nil {
utilhttp.HandleErr(w, errors.WithStack(fileExistsErr), http.StatusInternalServerError)
return
}
if fileExists {
http.ServeFile(w, r, filePath)
} else {
// this is in an else to prevent a: superfluous response.WriteHeader call
// TODO (aschie): Determine if this should do this on any 404, or if it should 404 a request
http.ServeFile(w, r, staticContent)
}
}
@ -53,7 +59,7 @@ func serveFile(w http.ResponseWriter, r *http.Request) {
func handleAuth(http.ResponseWriter, *http.Request) {
// TODO: handle the response body to capture the credentials
err := ws.WriteJSON(configs.WsMessage{
Type: configs.AirshipUI,
Type: configs.UI,
Component: configs.Authcomplete,
Timestamp: time.Now().UnixNano() / 1000000,
})
@ -77,6 +83,7 @@ func WebServer() {
webServerMux.HandleFunc("/ws", onOpen)
// establish routing to static angular client
log.Println("Attempting to serve static content from ", staticContent)
webServerMux.HandleFunc("/", serveFile)
// TODO: Figureout if we need to toggle the proxies on and off

View File

@ -31,12 +31,12 @@ const (
serverAddr string = "localhost:8080"
// client messages
initialize string = `{"type":"airshipui","component":"initialize"}`
keepalive string = `{"type":"airshipui","component":"keepalive"}`
initialize string = `{"type":"ui","component":"initialize"}`
keepalive string = `{"type":"ui","component":"keepalive"}`
unknownType string = `{"type":"fake_type","component":"initialize"}`
unknownComponent string = `{"type":"airshipui","component":"fake_component"}`
document string = `{"type":"airshipctl","component":"document","subcomponent":"getDefaults"}`
baremetal string = `{"type":"airshipctl","component":"baremetal","subcomponent":"getDefaults"}`
unknownComponent string = `{"type":"ui","component":"fake_component"}`
document string = `{"type":"ctl","component":"document","subcomponent":"getDefaults"}`
baremetal string = `{"type":"ctl","component":"baremetal","subcomponent":"getDefaults"}`
)
func init() {
@ -54,14 +54,12 @@ func TestHandleAuth(t *testing.T) {
_, err = http.Get("http://localhost:8080/auth")
require.NoError(t, err)
var response configs.WsMessage
err = client.ReadJSON(&response)
response, err := MessageReader(client)
require.NoError(t, err)
expected := configs.WsMessage{
Type: configs.AirshipUI,
Type: configs.UI,
Component: configs.Authcomplete,
// don't fail on timestamp diff
Timestamp: response.Timestamp,
}
@ -80,8 +78,24 @@ func NewTestClient() (*websocket.Conn, error) {
if err == nil {
return client, nil
}
time.Sleep(2 * time.Second)
time.Sleep(250 * time.Millisecond)
}
return nil, err
}
func MessageReader(client *websocket.Conn) (configs.WsMessage, error) {
var response configs.WsMessage
err := client.ReadJSON(&response)
// dump the initialize message that comes immediately from the backend
if response.Component == configs.Initialize {
response = configs.WsMessage{}
err = client.ReadJSON(&response)
}
if err != nil {
return response, err
}
return response, err
}

View File

@ -18,6 +18,7 @@ import (
"fmt"
"log"
"net/http"
"sync"
"time"
"github.com/gorilla/websocket"
@ -33,15 +34,16 @@ var upgrader = websocket.Upgrader{
// websocket that'll be reused by several places
var ws *websocket.Conn
var writeMutex sync.Mutex
// this is a way to allow for arbitrary messages to be processed by the backend
// the message of a specifc component is shunted to that subsystem for further processing
var functionMap = map[configs.WsRequestType]map[configs.WsComponentType]func(configs.WsMessage) configs.WsMessage{
configs.AirshipUI: {
configs.UI: {
configs.Keepalive: keepaliveReply,
configs.Initialize: clientInit,
},
configs.AirshipCTL: ctl.CTLFunctionMap,
configs.CTL: ctl.CTLFunctionMap,
}
// handle the origin request & upgrade to websocket
@ -68,6 +70,7 @@ func onOpen(response http.ResponseWriter, request *http.Request) {
}
go onMessage()
sendInit()
}
// handle messaging to the client
@ -83,33 +86,34 @@ func onMessage() {
break
}
// look through the function map to find the type to handle the request
if reqType, ok := functionMap[request.Type]; ok {
// the function map may have a component (function) to process the request
if component, ok := reqType[request.Component]; ok {
// get the response and tag the timestamp so it's not repeated across all functions
response := component(request)
response.Timestamp = time.Now().UnixNano() / 1000000
if err = ws.WriteJSON(response); err != nil {
onError(err)
break
// this has to be a go routine otherwise it will block any incoming messages waiting for a command return
go func() {
// look through the function map to find the type to handle the request
if reqType, ok := functionMap[request.Type]; ok {
// the function map may have a component (function) to process the request
if component, ok := reqType[request.Component]; ok {
// get the response and tag the timestamp so it's not repeated across all functions
response := component(request)
response.Timestamp = time.Now().UnixNano() / 1000000
if err = WebSocketSend(response); err != nil {
onError(err)
}
} else {
if err = WebSocketSend(requestErrorHelper(fmt.Sprintf("Requested component: %s, not found",
request.Component), request)); err != nil {
onError(err)
}
log.Printf("Requested component: %s, not found\n", request.Component)
}
} else {
if err = ws.WriteJSON(requestErrorHelper(fmt.Sprintf("Requested component: %s, not found",
request.Component), request)); err != nil {
if err = WebSocketSend(requestErrorHelper(fmt.Sprintf("Requested type: %s, not found",
request.Type), request)); err != nil {
onError(err)
break
}
log.Printf("Requested component: %s, not found\n", request.Component)
log.Printf("Requested type: %s, not found\n", request.Type)
}
} else {
if err = ws.WriteJSON(requestErrorHelper(fmt.Sprintf("Requested type: %s, not found",
request.Type), request)); err != nil {
onError(err)
break
}
log.Printf("Requested type: %s, not found\n", request.Type)
}
}()
}
}
@ -124,11 +128,19 @@ func onError(err error) {
log.Printf("Error receiving / sending message: %s\n", err)
}
// WebSocketSend allows for the sender to be thread safe, we cannot write to the websocket at the same time
func WebSocketSend(response configs.WsMessage) error {
writeMutex.Lock()
defer writeMutex.Unlock()
return ws.WriteJSON(response)
}
// The keepalive response including a timestamp from the server
// The UI will occasionally ping the server due to the websocket default timeout
func keepaliveReply(configs.WsMessage) configs.WsMessage {
return configs.WsMessage{
Type: configs.AirshipUI,
Type: configs.UI,
Component: configs.Keepalive,
}
}
@ -143,7 +155,19 @@ func requestErrorHelper(err string, request configs.WsMessage) configs.WsMessage
}
}
// this is generated on the onOpen event and sends the information the UI needs to startup
// sendInit is generated on the onOpen event and sends the information the UI needs to startup
func sendInit() {
response := clientInit(configs.WsMessage{
Timestamp: time.Now().UnixNano() / 1000000,
})
if err := WebSocketSend(response); err != nil {
onError(err)
}
}
// clientInit is in the function map if the client requests an init message this is the handler
// TODO (asciefe): determine if this is still necessary
func clientInit(configs.WsMessage) configs.WsMessage {
// if no auth method is supplied start with minimal functionality
if configs.UIConfig.AuthMethod == nil {
@ -151,7 +175,7 @@ func clientInit(configs.WsMessage) configs.WsMessage {
}
return configs.WsMessage{
Type: configs.AirshipUI,
Type: configs.UI,
Component: configs.Initialize,
IsAuthenticated: isAuthenticated,
Dashboards: configs.UIConfig.Dashboards,

View File

@ -17,6 +17,7 @@ package webservice
import (
"encoding/json"
"testing"
"time"
"opendev.org/airship/airshipui/util/utiltest"
@ -39,7 +40,7 @@ func TestClientInit(t *testing.T) {
require.NoError(t, err)
expected := configs.WsMessage{
Type: configs.AirshipUI,
Type: configs.UI,
Component: configs.Initialize,
IsAuthenticated: true,
Dashboards: utiltest.DummyDashboardsConfig(),
@ -65,10 +66,10 @@ func TestClientInitNoAuth(t *testing.T) {
require.NoError(t, err)
expected := configs.WsMessage{
Type: configs.AirshipUI,
Type: configs.UI,
Component: configs.Initialize,
// isAuthenticated should now be true in response
IsAuthenticated: true,
IsAuthenticated: response.IsAuthenticated,
Dashboards: []configs.Dashboard{
utiltest.DummyDashboardConfig(),
},
@ -89,7 +90,7 @@ func TestKeepalive(t *testing.T) {
require.NoError(t, err)
expected := configs.WsMessage{
Type: configs.AirshipUI,
Type: configs.UI,
Component: configs.Keepalive,
// don't fail on timestamp diff
Timestamp: response.Timestamp,
@ -126,7 +127,7 @@ func TestUnknownComponent(t *testing.T) {
require.NoError(t, err)
expected := configs.WsMessage{
Type: configs.AirshipUI,
Type: configs.UI,
Component: "fake_component",
// don't fail on timestamp diff
Timestamp: response.Timestamp,
@ -145,7 +146,7 @@ func TestHandleDocumentRequest(t *testing.T) {
require.NoError(t, err)
expected := configs.WsMessage{
Type: configs.AirshipCTL,
Type: configs.CTL,
Component: configs.Document,
SubComponent: configs.GetDefaults,
// don't fail on timestamp diff
@ -167,7 +168,7 @@ func TestHandleBaremetalRequest(t *testing.T) {
require.NoError(t, err)
expected := configs.WsMessage{
Type: configs.AirshipCTL,
Type: configs.CTL,
Component: configs.Baremetal,
SubComponent: configs.GetDefaults,
// don't fail on timestamp diff
@ -181,12 +182,20 @@ func TestHandleBaremetalRequest(t *testing.T) {
func getResponse(client *websocket.Conn, message string) (configs.WsMessage, error) {
err := client.WriteJSON(json.RawMessage(message))
time.Sleep(250 * time.Millisecond)
if err != nil {
return configs.WsMessage{}, err
}
var response configs.WsMessage
err = client.ReadJSON(&response)
if response.Component == configs.Initialize {
response = configs.WsMessage{}
err = client.ReadJSON(&response)
}
if err != nil {
return configs.WsMessage{}, err
}