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:
parent
0011d08420
commit
a7c518e4d6
@ -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"
|
||||
}
|
||||
|
@ -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 {
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>
|
@ -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();
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
@ -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 {
|
||||
}
|
||||
}
|
@ -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',
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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;
|
33
client/src/app/ctl/baremetal/baremetal.component.ts
Normal file
33
client/src/app/ctl/baremetal/baremetal.component.ts
Normal 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"));
|
||||
}
|
||||
}
|
10
client/src/app/ctl/ctl.component.ts
Normal file
10
client/src/app/ctl/ctl.component.ts
Normal 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 {
|
||||
|
||||
}
|
@ -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();
|
||||
});
|
@ -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%;
|
||||
}
|
53
client/src/app/ctl/document/document.component.html
Normal file
53
client/src/app/ctl/document/document.component.html
Normal 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>
|
138
client/src/app/ctl/document/document.component.ts
Normal file
138
client/src/app/ctl/document/document.component.ts
Normal 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"));
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
10
client/src/services/websocket/websocket.models.ts
Executable file
10
client/src/services/websocket/websocket.models.ts
Executable 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>;
|
||||
}
|
@ -13,4 +13,4 @@ describe('WebsocketService', () => {
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user