diff --git a/client/angular.json b/client/angular.json index 1aa388b..723634a 100644 --- a/client/angular.json +++ b/client/angular.json @@ -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" } diff --git a/client/src/app/airship/airship.component.ts b/client/src/app/airship/airship.component.ts deleted file mode 100644 index 2603c19..0000000 --- a/client/src/app/airship/airship.component.ts +++ /dev/null @@ -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 { - } - -} diff --git a/client/src/app/airship/bare-metal/bare-metal.component.ts b/client/src/app/airship/bare-metal/bare-metal.component.ts deleted file mode 100644 index b9d89cc..0000000 --- a/client/src/app/airship/bare-metal/bare-metal.component.ts +++ /dev/null @@ -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); - } -} diff --git a/client/src/app/airship/document/document-overview/document-overview.component.html b/client/src/app/airship/document/document-overview/document-overview.component.html deleted file mode 100644 index 832f66a..0000000 --- a/client/src/app/airship/document/document-overview/document-overview.component.html +++ /dev/null @@ -1,44 +0,0 @@ -
-
-
- - Source - Rendered - -
- - -
  • - - -
  • -
    - -
  • -
    - - {{node.name}} -
    -
      - -
    -
  • -
    -
    -
    -
    -
    -
    {{editorTitle}}
    -
    - - -
    -
    - -
    -
    diff --git a/client/src/app/airship/document/document-overview/document-overview.component.spec.ts b/client/src/app/airship/document/document-overview/document-overview.component.spec.ts deleted file mode 100644 index 1b1d5a4..0000000 --- a/client/src/app/airship/document/document-overview/document-overview.component.spec.ts +++ /dev/null @@ -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; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ DocumentOverviewComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(DocumentOverviewComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/client/src/app/airship/document/document-overview/document-overview.component.ts b/client/src/app/airship/document/document-overview/document-overview.component.ts deleted file mode 100644 index fbac030..0000000 --- a/client/src/app/airship/document/document-overview/document-overview.component.ts +++ /dev/null @@ -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(node => node.children); - dataSource = new MatTreeNestedDataSource(); - - 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; - } -} diff --git a/client/src/app/airship/document/document-pull/document-pull.component.css b/client/src/app/airship/document/document-pull/document-pull.component.css deleted file mode 100644 index e69de29..0000000 diff --git a/client/src/app/airship/document/document-pull/document-pull.component.html b/client/src/app/airship/document/document-pull/document-pull.component.html deleted file mode 100644 index 20878cc..0000000 --- a/client/src/app/airship/document/document-pull/document-pull.component.html +++ /dev/null @@ -1,3 +0,0 @@ - -

    Response to Pull: {{obby}}

    - diff --git a/client/src/app/airship/document/document-pull/document-pull.component.spec.ts b/client/src/app/airship/document/document-pull/document-pull.component.spec.ts deleted file mode 100644 index 8da654f..0000000 --- a/client/src/app/airship/document/document-pull/document-pull.component.spec.ts +++ /dev/null @@ -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; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ DocumentPullComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(DocumentPullComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/client/src/app/airship/document/document-pull/document-pull.component.ts b/client/src/app/airship/document/document-pull/document-pull.component.ts deleted file mode 100644 index ed97181..0000000 --- a/client/src/app/airship/document/document-pull/document-pull.component.ts +++ /dev/null @@ -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); - } - -} diff --git a/client/src/app/airship/document/document.component.css b/client/src/app/airship/document/document.component.css deleted file mode 100644 index e69de29..0000000 diff --git a/client/src/app/airship/document/document.component.html b/client/src/app/airship/document/document.component.html deleted file mode 100644 index a62ff07..0000000 --- a/client/src/app/airship/document/document.component.html +++ /dev/null @@ -1,5 +0,0 @@ - - diff --git a/client/src/app/airship/document/document.component.ts b/client/src/app/airship/document/document.component.ts deleted file mode 100644 index dbbd37b..0000000 --- a/client/src/app/airship/document/document.component.ts +++ /dev/null @@ -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 { - } -} diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index 0a5ca5d..79fbb6e 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts @@ -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', diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 258e2dc..c46d72f 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -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 { + 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 { diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 2f1603e..598621d 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -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, diff --git a/client/src/app/airship/airship.component.css b/client/src/app/ctl/baremetal/baremetal.component.css similarity index 100% rename from client/src/app/airship/airship.component.css rename to client/src/app/ctl/baremetal/baremetal.component.css diff --git a/client/src/app/airship/bare-metal/bare-metal.component.html b/client/src/app/ctl/baremetal/baremetal.component.html similarity index 100% rename from client/src/app/airship/bare-metal/bare-metal.component.html rename to client/src/app/ctl/baremetal/baremetal.component.html diff --git a/client/src/app/airship/bare-metal/bare-metal.component.spec.ts b/client/src/app/ctl/baremetal/baremetal.component.spec.ts similarity index 90% rename from client/src/app/airship/bare-metal/bare-metal.component.spec.ts rename to client/src/app/ctl/baremetal/baremetal.component.spec.ts index 1a49ba2..3510e84 100644 --- a/client/src/app/airship/bare-metal/bare-metal.component.spec.ts +++ b/client/src/app/ctl/baremetal/baremetal.component.spec.ts @@ -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; diff --git a/client/src/app/ctl/baremetal/baremetal.component.ts b/client/src/app/ctl/baremetal/baremetal.component.ts new file mode 100644 index 0000000..0cff564 --- /dev/null +++ b/client/src/app/ctl/baremetal/baremetal.component.ts @@ -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 { + 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")); + } +} diff --git a/client/src/app/airship/bare-metal/bare-metal.component.css b/client/src/app/ctl/ctl.component.css similarity index 100% rename from client/src/app/airship/bare-metal/bare-metal.component.css rename to client/src/app/ctl/ctl.component.css diff --git a/client/src/app/airship/airship.component.html b/client/src/app/ctl/ctl.component.html similarity index 100% rename from client/src/app/airship/airship.component.html rename to client/src/app/ctl/ctl.component.html diff --git a/client/src/app/ctl/ctl.component.ts b/client/src/app/ctl/ctl.component.ts new file mode 100644 index 0000000..ea483f5 --- /dev/null +++ b/client/src/app/ctl/ctl.component.ts @@ -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 { + +} diff --git a/client/src/app/airship/airship.component.spec.ts b/client/src/app/ctl/ctlcomponent.spec.ts similarity index 56% rename from client/src/app/airship/airship.component.spec.ts rename to client/src/app/ctl/ctlcomponent.spec.ts index 80cc848..7aabe65 100644 --- a/client/src/app/airship/airship.component.spec.ts +++ b/client/src/app/ctl/ctlcomponent.spec.ts @@ -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; +describe('CTLComponent', () => { + let component: CTLComponent; + let fixture: ComponentFixture; beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ AirshipComponent ] + declarations: [ CTLComponent ] }) .compileComponents(); })); beforeEach(() => { - fixture = TestBed.createComponent(AirshipComponent); + fixture = TestBed.createComponent(CTLComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/client/src/app/airship/document/document-overview/document-overview.component.css b/client/src/app/ctl/document/document.component.css similarity index 92% rename from client/src/app/airship/document/document-overview/document-overview.component.css rename to client/src/app/ctl/document/document.component.css index b390783..c9bca10 100644 --- a/client/src/app/airship/document/document-overview/document-overview.component.css +++ b/client/src/app/ctl/document/document.component.css @@ -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%; } \ No newline at end of file diff --git a/client/src/app/ctl/document/document.component.html b/client/src/app/ctl/document/document.component.html new file mode 100644 index 0000000..d71fc40 --- /dev/null +++ b/client/src/app/ctl/document/document.component.html @@ -0,0 +1,53 @@ + + +
    +
    +
    + + Source + Rendered + +
    + + +
  • + + +
  • +
    + +
  • +
    + + {{node.name}} +
    +
      + +
    +
  • +
    +
    +
    +
    +
    +
    {{editorTitle}}
    +
    + + +
    +
    + +
    +
    +
    + +
    + +

    Response to Pull: {{obby}}

    +
    +
    \ No newline at end of file diff --git a/client/src/app/airship/document/document.component.spec.ts b/client/src/app/ctl/document/document.component.spec.ts similarity index 100% rename from client/src/app/airship/document/document.component.spec.ts rename to client/src/app/ctl/document/document.component.spec.ts diff --git a/client/src/app/ctl/document/document.component.ts b/client/src/app/ctl/document/document.component.ts new file mode 100644 index 0000000..2282c62 --- /dev/null +++ b/client/src/app/ctl/document/document.component.ts @@ -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(node => node.children); + dataSource = new MatTreeNestedDataSource(); + + 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 { + 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")); + } +} + diff --git a/client/src/app/airship/document/document-overview/kustom-node.ts b/client/src/app/ctl/document/kustom-node.ts similarity index 100% rename from client/src/app/airship/document/document-overview/kustom-node.ts rename to client/src/app/ctl/document/kustom-node.ts diff --git a/client/src/app/dashboards/dashboards.component.ts b/client/src/app/dashboards/dashboards.component.ts index 76c91ae..8f87c20 100644 --- a/client/src/app/dashboards/dashboards.component.ts +++ b/client/src/app/dashboards/dashboards.component.ts @@ -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 { } diff --git a/client/src/services/notification/notification.service.spec.ts b/client/src/services/notification/notification.service.spec.ts deleted file mode 100644 index c4f2cd6..0000000 --- a/client/src/services/notification/notification.service.spec.ts +++ /dev/null @@ -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(); - }); -}); diff --git a/client/src/services/notification/notification.service.ts b/client/src/services/notification/notification.service.ts deleted file mode 100644 index f96bb8a..0000000 --- a/client/src/services/notification/notification.service.ts +++ /dev/null @@ -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); - } - } -} diff --git a/client/src/services/websocket/models/websocket-message/websocket-message.ts b/client/src/services/websocket/models/websocket-message/websocket-message.ts index 30f49bf..683f96d 100644 --- a/client/src/services/websocket/models/websocket-message/websocket-message.ts +++ b/client/src/services/websocket/models/websocket-message/websocket-message.ts @@ -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; + } } diff --git a/client/src/services/websocket/websocket.models.ts b/client/src/services/websocket/websocket.models.ts new file mode 100755 index 0000000..4e2ebc2 --- /dev/null +++ b/client/src/services/websocket/websocket.models.ts @@ -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; +} \ No newline at end of file diff --git a/client/src/services/websocket/websocket.service.spec.ts b/client/src/services/websocket/websocket.service.spec.ts index bf79772..dc3f132 100644 --- a/client/src/services/websocket/websocket.service.spec.ts +++ b/client/src/services/websocket/websocket.service.spec.ts @@ -13,4 +13,4 @@ describe('WebsocketService', () => { it('should be created', () => { expect(service).toBeTruthy(); }); -}); +}); \ No newline at end of file diff --git a/client/src/services/websocket/websocket.service.ts b/client/src/services/websocket/websocket.service.ts index b59e1d0..9865b32 100644 --- a/client/src/services/websocket/websocket.service.ts +++ b/client/src/services/websocket/websocket.service.ts @@ -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(); +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>(); + + // 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 { 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 { + 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(); + 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); + } + } } diff --git a/pkg/configs/configs.go b/pkg/configs/configs.go index 9bf9c5e..10e09e2 100644 --- a/pkg/configs/configs.go +++ b/pkg/configs/configs.go @@ -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 diff --git a/pkg/ctl/baremetal.go b/pkg/ctl/baremetal.go index df3f70f..def3465 100644 --- a/pkg/ctl/baremetal.go +++ b/pkg/ctl/baremetal.go @@ -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, } diff --git a/pkg/ctl/baremetal_test.go b/pkg/ctl/baremetal_test.go index dcc5c86..0cd3494 100644 --- a/pkg/ctl/baremetal_test.go +++ b/pkg/ctl/baremetal_test.go @@ -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", diff --git a/pkg/ctl/document.go b/pkg/ctl/document.go index 1c6a4b8..0fea2b2 100644 --- a/pkg/ctl/document.go +++ b/pkg/ctl/document.go @@ -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, } diff --git a/pkg/ctl/document_test.go b/pkg/ctl/document_test.go index feadbac..af5701e 100644 --- a/pkg/ctl/document_test.go +++ b/pkg/ctl/document_test.go @@ -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", diff --git a/pkg/webservice/alerts.go b/pkg/webservice/alerts.go index 8a845e5..d4adfdd 100644 --- a/pkg/webservice/alerts.go +++ b/pkg/webservice/alerts.go @@ -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) } } diff --git a/pkg/webservice/alerts_test.go b/pkg/webservice/alerts_test.go index cc02a0d..2235157 100644 --- a/pkg/webservice/alerts_test.go +++ b/pkg/webservice/alerts_test.go @@ -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{ diff --git a/pkg/webservice/server.go b/pkg/webservice/server.go index 2dccfe7..91ab275 100755 --- a/pkg/webservice/server.go +++ b/pkg/webservice/server.go @@ -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 diff --git a/pkg/webservice/server_test.go b/pkg/webservice/server_test.go index a6328b0..f0427bc 100644 --- a/pkg/webservice/server_test.go +++ b/pkg/webservice/server_test.go @@ -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 +} diff --git a/pkg/webservice/websocket.go b/pkg/webservice/websocket.go index 6cbdd7f..d3bf335 100644 --- a/pkg/webservice/websocket.go +++ b/pkg/webservice/websocket.go @@ -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, diff --git a/pkg/webservice/websocket_test.go b/pkg/webservice/websocket_test.go index 78e32f9..d766ea9 100644 --- a/pkg/webservice/websocket_test.go +++ b/pkg/webservice/websocket_test.go @@ -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 }