Adding Angular to UI

Angular was added to organize the FE
of the UI application better. In doing so I
have rebuilt the build scripts, added a routing
mechanism for the Go server to route and
serve the compiled TS pages from Angular.

Change-Id: I7ae2cacfd90372fa536b1639e5b54a8da786e2cd
This commit is contained in:
Danny Massa 2020-07-30 15:31:00 -05:00
parent d1a0509bf0
commit a0d1b40230
132 changed files with 15938 additions and 14545 deletions

47
.gitignore vendored
View File

@ -5,12 +5,49 @@ tools/*node*
# Generated binaries # Generated binaries
bin bin
dist
/tmp
/out-tsc
/build
*.exe
# Developer IDE # Only exists if Bazel was run
.idea/ /bazel-out
# Node modules # Node modules
web/node_modules node_modules
# Sphinx build venv # profiling files
.tox/ chrome-profiler-events*.json
speed-measure-plugin*.json
# IDEs and editors
.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db

View File

@ -9,12 +9,13 @@ SHELL=/bin/bash
GIT_VERSION=$(shell git describe --match 'v*' --always) GIT_VERSION=$(shell git describe --match 'v*' --always)
TOOLBINDIR := tools/bin TOOLBINDIR := tools/bin
WEBDIR := web WEBDIR := client
LINTER := $(TOOLBINDIR)/golangci-lint LINTER := $(TOOLBINDIR)/golangci-lint
LINTER_CONFIG := .golangci.yaml LINTER_CONFIG := .golangci.yaml
JSLINTER_BIN := $(realpath tools)/node-v12.16.3/bin NODEJS_BIN := $(realpath tools)/node-v12.16.3/bin
NPM := $(JSLINTER_BIN)/npm NPM := $(NODEJS_BIN)/npm
NPX := $(JSLINTER_BIN)/npx NPX := $(NODEJS_BIN)/npx
NG := $(NODEJS_BIN)/ng
# docker # docker
DOCKER_MAKE_TARGET := build DOCKER_MAKE_TARGET := build
@ -46,7 +47,7 @@ LD_FLAGS= '-X opendev.org/airship/airshipui/internal/commands.version=$(GIT_VERS
GO_FLAGS := -ldflags=$(LD_FLAGS) GO_FLAGS := -ldflags=$(LD_FLAGS)
BUILD_DIR := bin BUILD_DIR := bin
# Find all main.go files under cmd, excluding airshipui itself (which is the octant wrapper) # Find all main.go files under cmd, excluding airshipui itself
EXAMPLE_NAMES := $(notdir $(subst /main.go,,$(wildcard examples/*/main.go))) EXAMPLE_NAMES := $(notdir $(subst /main.go,,$(wildcard examples/*/main.go)))
EXAMPLES := $(addprefix $(BUILD_DIR)/, $(EXAMPLE_NAMES)) EXAMPLES := $(addprefix $(BUILD_DIR)/, $(EXAMPLE_NAMES))
MAIN := $(BUILD_DIR)/airshipui MAIN := $(BUILD_DIR)/airshipui
@ -66,9 +67,11 @@ DIRS = internal
RECURSIVE_DIRS = $(addprefix ./, $(addsuffix /..., $(DIRS))) RECURSIVE_DIRS = $(addprefix ./, $(addsuffix /..., $(DIRS)))
.PHONY: build .PHONY: build
build: $(MAIN) $(NPX) build: $(NPX) $(MAIN)
$(MAIN): FORCE $(MAIN): FORCE
@mkdir -p $(BUILD_DIR) @mkdir -p $(BUILD_DIR)
cd $(WEBDIR) && (PATH="$(PATH):$(NODEJS_BIN)"; $(NPM) install) && cd ..
cd $(WEBDIR) && (PATH="$(PATH):$(NODEJS_BIN)"; $(NG) build) && cd ..
go build -o $(MAIN)$(EXTENSION) $(GO_FLAGS) cmd/$(@F)/main.go go build -o $(MAIN)$(EXTENSION) $(GO_FLAGS) cmd/$(@F)/main.go
FORCE: FORCE:
@ -77,6 +80,9 @@ FORCE:
examples: $(EXAMPLES) examples: $(EXAMPLES)
$(EXAMPLES): FORCE $(EXAMPLES): FORCE
@mkdir -p $(BUILD_DIR) @mkdir -p $(BUILD_DIR)
./tools/install_npm
cd $(WEBDIR) && npm install && cd ..
cd $(WEBDIR) && ng build && cd ..
go build -o $@$(EXTENSION) $(GO_FLAGS) examples/$(@F)/main.go go build -o $@$(EXTENSION) $(GO_FLAGS) examples/$(@F)/main.go
.PHONY: install-octant-plugins .PHONY: install-octant-plugins
@ -84,9 +90,11 @@ install-octant-plugins:
@mkdir -p $(OCTANT_PLUGINSTUB_DIR) @mkdir -p $(OCTANT_PLUGINSTUB_DIR)
cp $(addsuffix $(EXTENSION), $(BUILD_DIR)/octant) $(OCTANT_PLUGINSTUB_DIR) cp $(addsuffix $(EXTENSION), $(BUILD_DIR)/octant) $(OCTANT_PLUGINSTUB_DIR)
.PHONY: install-npm-modules .PHONY: install-npm-modules
install-npm-modules: $(NPX) install-npm-modules: $(NPX)
cd $(WEBDIR) && (PATH="$(PATH):$(JSLINTER_BIN)"; $(NPM) install) && cd .. cd $(WEBDIR) && (PATH="$(PATH):$(NODEJS_BIN)"; $(NPM) install) && cd ..
.PHONY: test .PHONY: test
test: lint test: lint
@ -166,16 +174,16 @@ docs:
.PHONY: env .PHONY: env
.PHONY: lint .PHONY: lint
lint: tidy $(LINTER) $(NPX) lint: tidy $(LINTER)
@echo "Performing linting steps..." @echo "Performing linting steps..."
@echo "Running whitespace linting step..." @echo "Running whitespace linting step..."
@./tools/whitespace_linter @./tools/whitespace_linter
@echo "Running golangci-lint linting step..." @echo "Running golangci-lint linting step..."
$(LINTER) run --config $(LINTER_CONFIG) $(LINTER) run --config $(LINTER_CONFIG)
@echo "Running eslint for JavaScript linting step..." @echo "Installing NPM & running client linting step..."
cd $(WEBDIR) && (PATH="$(PATH):$(JSLINTER_BIN)"; $(NPX) --no-install eslint js) && cd .. ./tools/install_npm
@echo "Running eslint for HTML linting step..." cd $(WEBDIR) && (PATH="$(PATH):$(NODEJS_BIN)"; $(NPM) install) && cd ..
cd $(WEBDIR) && (PATH="$(PATH):$(JSLINTER_BIN)"; $(NPX) --no-install eslint --ext .html .) && cd .. cd $(WEBDIR) && (PATH="$(PATH):$(NODEJS_BIN)"; $(NG) build) && cd ..
@echo "Linting completed successfully" @echo "Linting completed successfully"
.PHONY: tidy .PHONY: tidy
@ -190,7 +198,7 @@ $(LINTER):
$(NPX): $(NPX):
@mkdir -p $(TOOLBINDIR) @mkdir -p $(TOOLBINDIR)
./tools/install_js_linter ./tools/install_npm
# add-copyright is a utility to add copyright header to missing files # add-copyright is a utility to add copyright header to missing files
.PHONY: add-copyright .PHONY: add-copyright

18
client/.browserslistrc Normal file
View File

@ -0,0 +1,18 @@
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
# For the full list of supported browsers by the Angular framework, please see:
# https://angular.io/guide/browser-support
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR
not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line.
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.

16
client/.editorconfig Normal file
View File

@ -0,0 +1,16 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
[*.md]
max_line_length = off
trim_trailing_whitespace = false

27
client/README.md Normal file
View File

@ -0,0 +1,27 @@
# AirshipuiUi
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.0.2.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

126
client/angular.json Normal file
View File

@ -0,0 +1,126 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"airshipui-ui": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/airshipui-ui",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"aot": true,
"assets": [
"src/airship-icon.svg",
"src/assets",
{ "glob": "**/*", "input": "node_modules/monaco-editor/min", "output": "./assets/monaco/" }
],
"styles": [
"src/styles.css",
"node_modules/ngx-toastr/toastr.css"
],
"scripts": []
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"extractCss": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "airshipui-ui:build"
},
"configurations": {
"production": {
"browserTarget": "airshipui-ui:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "airshipui-ui:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.spec.json",
"karmaConfig": "karma.conf.js",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.css"
],
"scripts": []
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"tsconfig.app.json",
"tsconfig.spec.json",
"e2e/tsconfig.json"
],
"exclude": [
"**/node_modules/**"
]
}
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "airshipui-ui:serve"
},
"configurations": {
"production": {
"devServerTarget": "airshipui-ui:serve:production"
}
}
}
}
}},
"defaultProject": "airshipui-ui"
}

View File

@ -0,0 +1,36 @@
// @ts-check
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');
/**
* @type { import("protractor").Config }
*/
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./src/**/*.e2e-spec.ts'
],
capabilities: {
browserName: 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json')
});
jasmine.getEnv().addReporter(new SpecReporter({
spec: {
displayStacktrace: StacktraceOption.PRETTY
}
}));
}
};

View File

@ -0,0 +1,23 @@
import { AppPage } from './app.po';
import { browser, logging } from 'protractor';
describe('workspace-project App', () => {
let page: AppPage;
beforeEach(() => {
page = new AppPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getTitleText()).toEqual('airshipui-ui app is running!');
});
afterEach(async () => {
// Assert that there are no errors emitted from the browser
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
expect(logs).not.toContain(jasmine.objectContaining({
level: logging.Level.SEVERE,
} as logging.Entry));
});
});

11
client/e2e/src/app.po.ts Normal file
View File

@ -0,0 +1,11 @@
import { browser, by, element } from 'protractor';
export class AppPage {
navigateTo(): Promise<unknown> {
return browser.get(browser.baseUrl) as Promise<unknown>;
}
getTitleText(): Promise<string> {
return element(by.css('app-root .content span')).getText() as Promise<string>;
}
}

14
client/e2e/tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es2018",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

32
client/karma.conf.js Normal file
View File

@ -0,0 +1,32 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular-devkit/build-angular/plugins/karma')
],
client: {
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/airshipui-ui'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true
});
};

13761
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

52
client/package.json Normal file
View File

@ -0,0 +1,52 @@
{
"name": "airshipui-ui",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "~10.0.3",
"@angular/cdk": "^10.0.1",
"@angular/common": "~10.0.3",
"@angular/compiler": "~10.0.3",
"@angular/core": "~10.0.3",
"@angular/flex-layout": "^9.0.0-beta.31",
"@angular/forms": "~10.0.3",
"@angular/material": "^10.0.1",
"@angular/platform-browser": "~10.0.3",
"@angular/platform-browser-dynamic": "~10.0.3",
"@angular/router": "~10.0.3",
"monaco-editor": "^0.20.0",
"ngx-toastr": "^13.0.0",
"rxjs": "~6.5.5",
"tslib": "^2.0.0",
"zone.js": "~0.10.3"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.1000.2",
"@angular/cli": "~10.0.2",
"@angular/compiler-cli": "~10.0.3",
"@types/jasmine": "~3.5.0",
"@types/jasminewd2": "~2.0.3",
"@types/node": "^12.11.1",
"codelyzer": "^6.0.0",
"eslint-plugin-html": "^6.0.2",
"jasmine-core": "~3.5.0",
"jasmine-spec-reporter": "~5.0.0",
"karma": "~5.0.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage-istanbul-reporter": "~3.0.2",
"karma-jasmine": "~3.3.0",
"karma-jasmine-html-reporter": "^1.5.0",
"protractor": "~7.0.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
"typescript": "~3.9.5"
}
}

View File

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="0.57in" height="0.5in" viewBox="0 0 40.89 36"><defs><style>.cls-1{fill:#65c7c2;}.cls-2{fill:#141f47;}</style></defs><title>Airship_Icon</title><path class="cls-1" d="M57.17,70.87l-2.34-1.33a2.78,2.78,0,0,1-1.36-1.89l-.88-4.48a40.81,40.81,0,0,1-2.47-5.59l-7.28,12.7a2.1,2.1,0,0,0,.77,2.87,2,2,0,0,0,1,.28h11.5a.74.74,0,0,0,.65-.39l1.07-1.88A3.41,3.41,0,0,1,57.17,70.87Z" transform="translate(-42.56 -37.43)"/><path class="cls-2" d="M64.83,38.48A2.1,2.1,0,0,0,62,37.72a2.05,2.05,0,0,0-.76.76L52.13,54.21a1.86,1.86,0,0,1,1.11-.75c2.95-.78,8.9-1.82,13.51.84A14.46,14.46,0,0,1,72,60h4.81a.64.64,0,0,1,.59.29Z" transform="translate(-42.56 -37.43)"/><path class="cls-1" d="M83.19,70.28,77.5,60.44a.76.76,0,0,1-.16.81l-3,3.06L83.45,71C83.37,70.73,83.29,70.51,83.19,70.28Z" transform="translate(-42.56 -37.43)"/><path class="cls-2" d="M62,62.52A42.66,42.66,0,0,1,52,54.4l-.16.29a1.49,1.49,0,0,0,0,1.24,25.13,25.13,0,0,0,2.76,6.49l.94,4.84a.68.68,0,0,0,.36.52l2.34,1.33a.72.72,0,0,0,1-.22l0-.07L60,67.56a15,15,0,0,0,6.83,1.33L69.22,73a.76.76,0,0,0,1,.29.81.81,0,0,0,.36-.45L72,68.24a6.14,6.14,0,0,0,.84-.23A1.94,1.94,0,0,0,74,67.07,42.25,42.25,0,0,1,62,62.52Z" transform="translate(-42.56 -37.43)"/><path class="cls-1" d="M82.05,73.3l-8.41-3.7-1.07,3.8h8.8A1.14,1.14,0,0,0,82.05,73.3Z" transform="translate(-42.56 -37.43)"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1 @@
<router-outlet></router-outlet>

View File

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

View File

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

View File

@ -0,0 +1 @@
<button mat-raised-button color="accent" (click)="generateIso()">Generate ISO</button>

View File

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

View File

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

View File

@ -0,0 +1 @@
<div id="DocOverviewDiv" style="height: 60vh;overflow: hidden;border:1px solid grey"></div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,47 @@
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';
const routes: Routes = [
{
path: 'airship',
component: AirshipComponent,
children: [
{
path: 'bare-metal',
component: BareMetalComponent
}, {
path: 'documents',
component: DocumentComponent,
children: [
{
path: 'overview',
component: DocumentOverviewComponent
}, {
path: 'pull',
component: DocumentPullComponent
}]
}]
}, {
path: 'dashboard',
component: DashboardsComponent
}, {
path: '',
component: HomeComponent
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {
}

View File

@ -0,0 +1,67 @@
.main-container {
display: flex;
flex-direction: column;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.sidenav-container {
flex: 1 1 auto;
}
.sidenav-content {
width: 250px;
}
.mat-expansion-panel-header{
padding-right: 16px;
padding-left: 16px;
}
.mat-expansion-panel:not([class*='mat-elevation-z']) {
box-shadow: none;
}
.mat-expansion-panel-body {
padding: 0;
}
.spacer {
flex: 1 1 auto;
}
.icon-container {
width: 48px;
height: 48px;
}
.mat-sidenav-content {
min-height: 100%;
display: flex;
flex-direction: column;
}
.header-padding {
height: 64px;
}
.page-body {
flex-grow: 1;
}
.toolbar-footer {
flex-shrink: 0;
z-index: 2;
}
.toolbar-header {
flex-shrink: 0;
z-index: 2;
}
.h3 {
font-size: 12px;
}

View File

@ -0,0 +1,71 @@
<div class="main-container">
<mat-sidenav-container class="sidenav-container">
<mat-sidenav class="sidenav-content" #snav [mode]="'side'">
<mat-nav-list>
<svg width="249" height="46">
<use xlink:href="assets/logo/airship-horizontal-logo.svg#Layer_1"></use>
</svg>
<span *ngFor="let item of menu">
<span *ngIf="item.children && item.children.length > 0">
<mat-accordion>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title>
<div fxLayout="row" fxLayoutAlign="center center" >
<div *ngIf="item.iconName" class="icon-container" fxLayoutAlign="center center">
<mat-icon svgIcon="{{ item.iconName }}"></mat-icon>
</div>
{{ item.displayName }}
</div>
</mat-panel-title>
</mat-expansion-panel-header>
<span *ngFor="let child of item.children">
<mat-list-item *ngIf="!child.external" routerLink="{{ child.route }}">
<div fxLayout="row" fxLayoutAlign="center center" >
<div *ngIf="child.iconName" class="icon-container" fxLayoutAlign="center center">
<mat-icon svgIcon="{{ child.iconName }}"></mat-icon>
</div>
{{ child.displayName }}
</div>
</mat-list-item>
<mat-list-item *ngIf="child.external" (click)="openLink(child.route)">
<div fxLayout="row" fxLayoutAlign="center center" >
<div *ngIf="child.iconName" class="icon-container" fxLayoutAlign="center center">
<mat-icon svgIcon=launch></mat-icon>
</div>
{{ child.displayName }}
</div>
</mat-list-item>
</span>
</mat-expansion-panel>
</mat-accordion>
</span>
<span *ngIf="!item.children || item.children.length === 0">
<mat-list-item routerLink="{{ item.route }}">
<div fxLayout="row" fxLayoutAlign="center center">
<div *ngIf="item.iconName" class="icon-container" fxLayoutAlign="center center">
<mat-icon svgIcon="{{ item.iconName }}"></mat-icon>
</div>
{{item.displayName}}
</div>
</mat-list-item>
</span>
</span>
</mat-nav-list>
</mat-sidenav>
<mat-sidenav-content>
<mat-toolbar color="primary" class="toolbar-header">
<button mat-icon-button (click)="snav.toggle()"><mat-icon svgIcon="list"></mat-icon></button>
<span class="spacer"></span>
<button mat-icon-button><mat-icon svgIcon="account"></mat-icon></button>
</mat-toolbar>
<router-outlet></router-outlet>
<span class="page-body"></span>
<mat-toolbar class="toolbar-footer">
<h3>Airship UI &copy; {{ this.currentYear }}</h3>
<span class="spacer"></span>
<h3>Version: {{ this.version }}</h3>
</mat-toolbar>
</mat-sidenav-content>
</mat-sidenav-container>
</div>

View File

@ -0,0 +1,31 @@
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'airshipui-ui'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect('airshipui-ui').toEqual('airshipui-ui');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain('airshipui-ui app is running!');
});
});

View File

@ -0,0 +1,73 @@
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';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnDestroy, OnInit {
currentYear: number;
version: string;
menu: NavInterface [] = [
{
displayName: 'Airship',
iconName: 'airplane',
children: [
{
displayName: 'Bare Metal',
route: 'airship/bare-metal',
iconName: 'server'
}, {
displayName: 'Documents',
route: 'airship/documents/overview',
iconName: 'doc'
}]
}, {
displayName: 'Dashboards',
iconName: 'speed',
}];
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);
}
});
}
ngOnDestroy(): void {
}
ngOnInit(): void {
this.iconService.registerIcons();
}
updateDashboards(dashboards: Dashboard[]): void {
if (this.menu[1].children === undefined) {
this.menu[1].children = [];
}
dashboards.forEach((dashboard) => {
const navInterface = new NavInterface();
navInterface.displayName = dashboard.name;
navInterface.route = dashboard.baseURL;
navInterface.external = true;
this.menu[1].children.push(navInterface);
});
}
openLink(url: string): void {
window.open(url, '_blank');
}
}

View File

@ -0,0 +1,65 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatListModule } from '@angular/material/list';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
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 { 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 { 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';
import {FormsModule} from '@angular/forms';
@NgModule({
declarations: [
AppComponent,
AirshipComponent,
DashboardsComponent,
HomeComponent,
BareMetalComponent,
DocumentComponent,
DocumentOverviewComponent,
DocumentPullComponent,
],
imports: [
AppRoutingModule,
BrowserModule,
BrowserAnimationsModule,
FlexLayoutModule,
FormsModule,
MatToolbarModule,
MatSidenavModule,
MatListModule,
MatIconModule,
MatButtonModule,
MatTableModule,
MatCheckboxModule,
MatExpansionModule,
HttpClientModule,
RouterModule,
MatTabsModule,
ToastrModule.forRoot()
],
providers: [WebsocketService],
bootstrap: [AppComponent]
})
export class AppModule { }

View File

@ -0,0 +1 @@
<p>dashboards works!</p>

View File

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

View File

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

View File

View File

View File

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

View File

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

View File

@ -0,0 +1,8 @@
export class NavInterface {
displayName: string;
disabled?: boolean;
iconName?: string;
route?: string;
external?: boolean;
children?: NavInterface[];
}

View File

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M3 5v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2H5c-1.11 0-2 .9-2 2zm12 4c0 1.66-1.34 3-3 3s-3-1.34-3-3 1.34-3 3-3 3 1.34 3 3zm-9 8c0-2 4-3.1 6-3.1s6 1.1 6 3.1v1H6v-1z"/></svg>

After

Width:  |  Height:  |  Size: 314 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><path d="M22,16v-2l-8.5-5V3.5C13.5,2.67,12.83,2,12,2s-1.5,0.67-1.5,1.5V9L2,14v2l8.5-2.5V19L8,20.5L8,22l4-1l4,1l0-1.5L13.5,19 v-5.5L22,16z"/><path d="M0,0h24v24H0V0z" fill="none"/></g></svg>

After

Width:  |  Height:  |  Size: 309 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>

After

Width:  |  Height:  |  Size: 264 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>

After

Width:  |  Height:  |  Size: 268 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z"/></svg>

After

Width:  |  Height:  |  Size: 224 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 13H4c-.55 0-1 .45-1 1v6c0 .55.45 1 1 1h16c.55 0 1-.45 1-1v-6c0-.55-.45-1-1-1zM7 19c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zM20 3H4c-.55 0-1 .45-1 1v6c0 .55.45 1 1 1h16c.55 0 1-.45 1-1V4c0-.55-.45-1-1-1zM7 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/></svg>

After

Width:  |  Height:  |  Size: 395 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M20.38 8.57l-1.23 1.85a8 8 0 0 1-.22 7.58H5.07A8 8 0 0 1 15.58 6.85l1.85-1.23A10 10 0 0 0 3.35 19a2 2 0 0 0 1.72 1h13.85a2 2 0 0 0 1.74-1 10 10 0 0 0-.27-10.44zm-9.79 6.84a2 2 0 0 0 2.83 0l5.66-8.49-8.49 5.66a2 2 0 0 0 0 2.83z"/></svg>

After

Width:  |  Height:  |  Size: 364 B

View File

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -0,0 +1,4 @@
export const environment = {
production: true,
version: 'Development'
};

View File

@ -0,0 +1,17 @@
// This file can be replaced during build by using the `fileReplacements` array.
// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false,
version: 'Development'
};
/*
* For easier debugging in development mode, you can import the following file
* to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
*
* This import should be commented out in production mode because it will have a negative impact
* on performance if an error is thrown.
*/
// import 'zone.js/dist/zone-error'; // Included with Angular CLI.

13
client/src/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Airship UI</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="airship-icon.svg">
</head>
<body>
<app-root></app-root>
</body>
</html>

12
client/src/main.ts Normal file
View File

@ -0,0 +1,12 @@
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.error(err));

63
client/src/polyfills.ts Normal file
View File

@ -0,0 +1,63 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/

View File

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

View File

@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { MatIconRegistry } from '@angular/material/icon';
import { DomSanitizer } from '@angular/platform-browser';
import { Icons } from 'src/services/icon/icons.enum';
@Injectable({
providedIn: 'root'
})
export class IconService {
constructor(
private matIconRegistry: MatIconRegistry,
private domSanitizer: DomSanitizer
) { }
public registerIcons(): void {
this.loadIcons(Object.values(Icons), '../assets/icons');
}
private loadIcons(iconKeys: string[], iconUrl: string): void {
iconKeys.forEach(key => {
this.matIconRegistry.addSvgIcon(key, this.domSanitizer.bypassSecurityTrustResourceUrl(`${iconUrl}/${key}.svg`));
});
}
}

View File

@ -0,0 +1,9 @@
export enum Icons {
account = 'account',
airplane = 'airplane',
doc = 'doc',
list = 'list',
server = 'server',
speed = 'speed',
launch = 'launch'
}

View File

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

View File

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

View File

@ -0,0 +1,7 @@
import { Dashboard } from './dashboard';
describe('Dashboard', () => {
it('should create an instance', () => {
expect(new Dashboard()).toBeTruthy();
});
});

View File

@ -0,0 +1,9 @@
import {Executable} from './executable/executable';
export class Dashboard {
name: string;
baseURL: string;
path: string;
isProxied: boolean;
executable: Executable;
}

View File

@ -0,0 +1,7 @@
import { Executable } from './executable';
describe('Executable', () => {
it('should create an instance', () => {
expect(new Executable()).toBeTruthy();
});
});

View File

@ -0,0 +1,5 @@
export class Executable {
autoStart: boolean;
filePath: string;
args: string[];
}

View File

@ -0,0 +1,7 @@
import { WebsocketMessage } from './websocket-message';
describe('WebsocketMessage', () => {
it('should create an instance', () => {
expect(new WebsocketMessage()).toBeTruthy();
});
});

View File

@ -0,0 +1,16 @@
import {Dashboard} from './dashboard/dashboard';
export class WebsocketMessage {
type: string;
component: string;
subComponent: string;
timestamp: number;
dashboards: Dashboard[];
error: string;
fade: boolean;
html: string;
isAuthenticated: boolean;
message: string;
data: JSON;
yaml: string;
}

View File

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

View File

@ -0,0 +1,194 @@
import { Injectable } 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';
@Injectable({
providedIn: 'root'
})
export class WebsocketService {
public subject = new Subject<WebsocketMessage>();
private ws: WebSocket;
private timeout: number;
private static convertIncomingMessageJsonToObject(incomingMessage: string): WebsocketMessage {
const json = JSON.parse(incomingMessage);
const messageTransform = new WebsocketMessage();
if (typeof json.type === 'string') {
messageTransform.type = json.type;
}
if (typeof json.component === 'string') {
messageTransform.component = json.component;
}
if (typeof json.subComponent === 'string') {
messageTransform.subComponent = json.subComponent;
}
if (typeof json.timestamp === 'number') {
messageTransform.timestamp = json.timestamp;
}
if (typeof json.error === 'string') {
messageTransform.error = json.error;
}
if (typeof json.fade === 'boolean') {
messageTransform.fade = json.fade;
}
if (typeof json.html === 'string') {
messageTransform.html = json.html;
}
if (typeof json.isAuthenticated === 'boolean') {
messageTransform.isAuthenticated = json.isAuthenticated;
}
if (typeof json.message === 'string') {
messageTransform.message = json.message;
}
if (typeof json.data === 'string' && JSON.parse(json.data)) {
messageTransform.data = JSON.parse(json.data);
}
if (typeof json.yaml === 'string') {
messageTransform.yaml = json.yaml;
}
if (typeof json.dashboards !== undefined && Array.isArray(json.dashboards)) {
json.dashboards.forEach(dashboard => {
const dashboardTransform = new Dashboard();
if (typeof dashboard.name === 'string') {
dashboardTransform.name = dashboard.name;
}
if (typeof dashboard.baseURL === 'string') {
dashboardTransform.baseURL = dashboard.baseURL;
}
if (typeof dashboard.path === 'string') {
dashboardTransform.path = dashboard.path;
}
if (typeof dashboard.isProxied === 'boolean') {
dashboardTransform.isProxied = dashboard.isProxied;
}
if (typeof dashboard.executable === 'object') {
const executableTransform = new Executable();
if (typeof dashboard.executable.autoStart === 'boolean') {
executableTransform.autoStart = dashboard.executable.autoStart;
}
if (typeof dashboard.executable.filePath === 'string') {
executableTransform.filePath = dashboard.executable.filePath;
}
if (typeof dashboard.executable.args !== undefined && Array.isArray(typeof dashboard.executable.args)) {
dashboard.executable.args.forEach(arg => {
if (typeof arg === 'string') {
executableTransform.args.push(arg);
}
});
}
}
if (messageTransform.dashboards === undefined) {
messageTransform.dashboards = [];
}
messageTransform.dashboards.push(dashboardTransform);
});
}
return messageTransform;
}
constructor() {
this.register();
}
public sendMessage(message: WebsocketMessage): void {
message.timestamp = new Date().getTime();
this.ws.send(JSON.stringify(message));
}
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.convertIncomingMessageJsonToObject(event.data));
};
this.ws.onerror = (event) => {
console.log('Web Socket received an error: ', event);
};
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();
};
this.ws.onclose = (event) => {
this.close(event.code);
};
}
private close(code): void {
switch (code) {
case 1000:
console.log('Web Socket Closed: Normal closure: ', code);
break;
case 1001:
console.log('Web Socket Closed: An endpoint is "going away", such as a server going down or a browser having navigated away from a page:', code);
break;
case 1002:
console.log('Web Socket Closed: terminating the connection due to a protocol error: ', code);
break;
case 1003:
console.log('Web Socket Closed: terminating the connection because it has received a type of data it cannot accept: ', code);
break;
case 1004:
console.log('Web Socket Closed: Reserved. The specific meaning might be defined in the futur: ', code);
break;
case 1005:
console.log('Web Socket Closed: No status code was actually present: ', code);
break;
case 1006:
console.log('Web Socket Closed: The connection was closed abnormally: ', code);
break;
case 1007:
console.log('Web Socket Closed: terminating the connection because it has received data within a message that was not ' +
'consistent with the type of the message: ', code);
break;
case 1008:
console.log('Web Socket Closed: terminating the connection because it has received a message that "violates its policy": ', code);
break;
case 1009:
console.log('Web Socket Closed: terminating the connection because it has received a message that is too big for it to ' +
'process: ', code);
break;
case 1010:
console.log('Web Socket Closed: client is terminating the connection because it has expected the server to negotiate ' +
'one or more extension, but the server didn\'t return them in the response message of the WebSocket handshake: ', code);
break;
case 1011:
console.log('Web Socket Closed: server is terminating the connection because it encountered an unexpected condition that' +
' prevented it from fulfilling the request: ', code);
break;
case 1015:
console.log('Web Socket Closed: closed due to a failure to perform a TLS handshake (e.g., the server certificate can\'t be' +
' verified): ', code);
break;
default:
console.log('Web Socket Closed: unknown error code: ', code);
break;
}
this.ws = null;
}
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' };
this.ws.send(JSON.stringify(json));
this.timeout = window.setTimeout(this.keepAlive, 60000);
}
}
}

15
client/src/styles.css Normal file
View File

@ -0,0 +1,15 @@
@import '~@angular/material/prebuilt-themes/indigo-pink.css';
body {
font-family: Roboto, Arial, sans-serif;
margin: 0;
}
.basic-container {
padding: 30px;
}
.version-info {
font-size: 8pt;
float: right;
}

25
client/src/test.ts Normal file
View File

@ -0,0 +1,25 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(path: string, deep?: boolean, filter?: RegExp): {
keys(): string[];
<T>(id: string): T;
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

15
client/tsconfig.app.json Normal file
View File

@ -0,0 +1,15 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"files": [
"src/main.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.d.ts"
]
}

20
client/tsconfig.base.json Normal file
View File

@ -0,0 +1,20 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
"module": "es2020",
"lib": [
"es2018",
"dom"
]
}
}

20
client/tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
/*
This is a "Solution Style" tsconfig.json file, and is used by editors and TypeScripts language server to improve development experience.
It is not intended to be used to perform a compilation.
To learn more about this file see: https://angular.io/config/solution-tsconfig.
*/
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
},
{
"path": "./e2e/tsconfig.json"
}
]
}

18
client/tsconfig.spec.json Normal file
View File

@ -0,0 +1,18 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": [
"jasmine"
]
},
"files": [
"src/test.ts",
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}

152
client/tslint.json Normal file
View File

@ -0,0 +1,152 @@
{
"extends": "tslint:recommended",
"rules": {
"align": {
"options": [
"parameters",
"statements"
]
},
"array-type": false,
"arrow-return-shorthand": true,
"curly": true,
"deprecation": {
"severity": "warning"
},
"component-class-suffix": true,
"contextual-lifecycle": true,
"directive-class-suffix": true,
"directive-selector": [
true,
"attribute",
"app",
"camelCase"
],
"component-selector": [
true,
"element",
"app",
"kebab-case"
],
"eofline": true,
"import-blacklist": [
true,
"rxjs/Rx"
],
"import-spacing": true,
"indent": {
"options": [
"spaces"
]
},
"max-classes-per-file": false,
"max-line-length": [
true,
140
],
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-empty": false,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-non-null-assertion": true,
"no-redundant-jsdoc": true,
"no-switch-case-fall-through": true,
"no-var-requires": false,
"object-literal-key-quotes": [
true,
"as-needed"
],
"quotemark": [
true,
"single"
],
"semicolon": {
"options": [
"always"
]
},
"space-before-function-paren": {
"options": {
"anonymous": "never",
"asyncArrow": "always",
"constructor": "never",
"method": "never",
"named": "never"
}
},
"typedef": [
true,
"call-signature"
],
"typedef-whitespace": {
"options": [
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
},
{
"call-signature": "onespace",
"index-signature": "onespace",
"parameter": "onespace",
"property-declaration": "onespace",
"variable-declaration": "onespace"
}
]
},
"variable-name": {
"options": [
"ban-keywords",
"check-format",
"allow-pascal-case"
]
},
"whitespace": {
"options": [
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type",
"check-typecast"
]
},
"no-conflicting-lifecycle": true,
"no-host-metadata-property": true,
"no-input-rename": true,
"no-inputs-metadata-property": true,
"no-output-native": true,
"no-output-on-prefix": true,
"no-output-rename": true,
"no-outputs-metadata-property": true,
"template-banana-in-box": true,
"template-no-negated-async": true,
"use-lifecycle-interface": true,
"use-pipe-transform-interface": true
},
"rulesDirectory": [
"codelyzer"
]
}

0
examples/authentication/main.go Executable file → Normal file
View File

170
examples/authentication/templates/index.html Executable file → Normal file
View File

@ -1,86 +1,86 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>AirshipUI Test {{.Title}}</title> <title>AirshipUI Test {{.Title}}</title>
<link rel="icon" href="data:;base64,="> <link rel="icon" href="data:;base64,=">
</head> </head>
<script> <script>
function testIt() { function testIt() {
document.getElementById("AuthBtn").disabled = true; document.getElementById("AuthBtn").disabled = true;
console.log(window.location.pathname); console.log(window.location.pathname);
let xhr = new XMLHttpRequest(); let xhr = new XMLHttpRequest();
xhr.open("POST", window.location.pathname); xhr.open("POST", window.location.pathname);
xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhr.onload = function () { xhr.onload = function () {
if (this.status === 201) { if (this.status === 201) {
document.cookie = "airshipUI=" + xhr.response + "expires=" + new Date().getUTCDate; document.cookie = "airshipUI=" + xhr.response + "expires=" + new Date().getUTCDate;
console.log(JSON.parse(xhr.response)); console.log(JSON.parse(xhr.response));
} else { } else {
console.log({ console.log({
status: this.status, status: this.status,
statusText: xhr.statusText statusText: xhr.statusText
}); });
document.getElementById("OutputDiv").innerHTML = '<span style="color:red"><h2>&#9760; ID or Password is incorrect please try again &#9760;</h2></span>'; document.getElementById("OutputDiv").innerHTML = '<span style="color:red"><h2>&#9760; ID or Password is incorrect please try again &#9760;</h2></span>';
document.getElementById("AuthBtn").disabled = false; document.getElementById("AuthBtn").disabled = false;
} }
}; };
xhr.onerror = function () { xhr.onerror = function () {
reject({ reject({
status: this.status, status: this.status,
statusText: xhr.statusText statusText: xhr.statusText
}); });
}; };
let json = JSON.stringify({"id": document.getElementById("ID").value, "password": document.getElementById("Passwd").value}); let json = JSON.stringify({"id": document.getElementById("ID").value, "password": document.getElementById("Passwd").value});
console.log(json) console.log(json)
xhr.send(json); xhr.send(json);
} }
</script> </script>
<body> <body>
<h1>Airship UI Test {{.Title}}</h1> <h1>Airship UI Test {{.Title}}</h1>
<table> <table>
<tr> <tr>
<td> <td>
<b>Id:</b>&nbsp;&nbsp; <b>Id:</b>&nbsp;&nbsp;
</td> </td>
<td> <td>
<input type="text" id="ID"> <input type="text" id="ID">
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
<b>Password:</b>&nbsp;&nbsp; <b>Password:</b>&nbsp;&nbsp;
</td> </td>
<td> <td>
<input type="password" id="Passwd"> <input type="password" id="Passwd">
</td> </td>
</tr> </tr>
<tr> <tr>
<td colspan="2"> <td colspan="2">
<button id="AuthBtn" onclick="testIt()">Test It!</button> <button id="AuthBtn" onclick="testIt()">Test It!</button>
</td> </td>
</tr> </tr>
</table> </table>
<div id="OutputDiv"></div> <div id="OutputDiv"></div>
<h2>&#9888; Warning! &#9888;</h2> <h2>&#9888; Warning! &#9888;</h2>
<p>This is a {{.Title}} test page is only intended as an example for how to use {{.Title}} with AirshipUI.</p> <p>This is a {{.Title}} test page is only intended as an example for how to use {{.Title}} with AirshipUI.</p>
<p>The System will return the following HTML status codes and responses</p> <p>The System will return the following HTML status codes and responses</p>
<ul> <ul>
{{if eq .Title "Basic Auth"}} {{if eq .Title "Basic Auth"}}
<li>201: Created. The password attempt was successful and the backend has sent an xauth token header to AirshipUI.</li> <li>201: Created. The password attempt was successful and the backend has sent an xauth token header to AirshipUI.</li>
{{else if eq .Title "Cookie"}} {{else if eq .Title "Cookie"}}
<li>201: Created. The password attempt was successful and the backend has set a cookie and sent the cookie contents to AirshipUI.</li> <li>201: Created. The password attempt was successful and the backend has set a cookie and sent the cookie contents to AirshipUI.</li>
{{else if eq .Title "OAuth"}} {{else if eq .Title "OAuth"}}
<li>201: Created. The password attempt was successful and the backend has set a JWT (JSON Web Token) and sent the JWT contents to AirshipUI.</li> <li>201: Created. The password attempt was successful and the backend has set a JWT (JSON Web Token) and sent the JWT contents to AirshipUI.</li>
{{end}} {{end}}
<li>400: Bad request. There was an error sending the system the authentication request, most likely bad JSON.</li> <li>400: Bad request. There was an error sending the system the authentication request, most likely bad JSON.</li>
<li>401: Unauthorized. Bad id / password attempt.</li> <li>401: Unauthorized. Bad id / password attempt.</li>
<li>403: Forbidden. The id / password combination was correct but the id is not allowed for the resource.</li> <li>403: Forbidden. The id / password combination was correct but the id is not allowed for the resource.</li>
<li>500: Internal Server Error. There was a processing error on the back end.</li> <li>500: Internal Server Error. There was a processing error on the back end.</li>
</ul> </ul>
</body> </body>
</html> </html>

0
examples/octant/main.go Executable file → Normal file
View File

9
go.mod
View File

@ -5,10 +5,13 @@ go 1.13
require ( require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/gorilla/websocket v1.4.2 github.com/gorilla/websocket v1.4.2
github.com/spf13/cobra v0.0.6 github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.4.0 github.com/spf13/cobra v0.0.7
github.com/stretchr/testify v1.6.1
github.com/vmware-tanzu/octant v0.12.0 github.com/vmware-tanzu/octant v0.12.0
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 golang.org/x/net v0.0.0-20200625001655-4c5254603344 // indirect
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 // indirect
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f
opendev.org/airship/airshipctl v0.0.0-20200518155418-7276dd68d8d0 opendev.org/airship/airshipctl v0.0.0-20200518155418-7276dd68d8d0
) )

22
go.sum
View File

@ -158,6 +158,7 @@ github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwC
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4=
github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=
github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
@ -455,9 +456,11 @@ github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7
github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s= github.com/matoous/godox v0.0.0-20190911065817-5d6d842e92eb/go.mod h1:1BELzlh859Sh1c6+90blK8lbYy0kwQf1bYlBhBysy1s=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw=
@ -615,6 +618,8 @@ github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs= github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs=
github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/cobra v0.0.7 h1:FfTH+vuMXOas8jmfb5/M7dzEYx7LpcLb7a0LPe34uOU=
github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
@ -638,6 +643,8 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
@ -668,6 +675,7 @@ go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.mongodb.org/mongo-driver v1.1.2 h1:jxcFYjlkl8xaERsgLo+RNquI0epW6zuy/ZRQs6jnrFA=
go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@ -698,6 +706,8 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -735,6 +745,8 @@ golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
@ -748,6 +760,8 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -777,6 +791,9 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8=
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -817,6 +834,7 @@ golang.org/x/tools v0.0.0-20190719005602-e377ae9d6386/go.mod h1:jcCCGcm9btYwXyDq
golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190910044552-dd2b5c81c578/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190930201159-7c411dea38b0/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190930201159-7c411dea38b0/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191010075000-0337d82405ff h1:XdBG6es/oFDr1HwaxkxgVve7NB281QhxgK/i4voubFs=
golang.org/x/tools v0.0.0-20191010075000-0337d82405ff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191010075000-0337d82405ff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
@ -851,6 +869,7 @@ google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ij
google.golang.org/grpc v1.23.0 h1:AzbTB6ux+okLTzP8Ru1Xs41C303zdcfEht7MQnYJt5A= google.golang.org/grpc v1.23.0 h1:AzbTB6ux+okLTzP8Ru1Xs41C303zdcfEht7MQnYJt5A=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
@ -887,6 +906,8 @@ gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@ -979,6 +1000,7 @@ sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5
sigs.k8s.io/kustomize/api v0.3.1 h1:oqMIXvS6tFEUVuKIRUKDa05eC4Hh+cb9JYg8Zhp2d24= sigs.k8s.io/kustomize/api v0.3.1 h1:oqMIXvS6tFEUVuKIRUKDa05eC4Hh+cb9JYg8Zhp2d24=
sigs.k8s.io/kustomize/api v0.3.1/go.mod h1:A+ATnlHqzictQfQC1q3KB/T6MSr0UWQsrrLxMWkge2E= sigs.k8s.io/kustomize/api v0.3.1/go.mod h1:A+ATnlHqzictQfQC1q3KB/T6MSr0UWQsrrLxMWkge2E=
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06 h1:zD2IemQ4LmOcAumeiyDWXKUI2SO0NYDe3H6QGvPOVgU=
sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18= sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18=
sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=

0
internal/configs/configs.go Executable file → Normal file
View File

View File

@ -11,15 +11,13 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package configs_test package configs
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"opendev.org/airship/airshipui/internal/configs"
"opendev.org/airship/airshipui/testutil"
) )
const ( const (
@ -28,22 +26,62 @@ const (
invalidTestFile string = "testdata/airshipui_invalid.json" invalidTestFile string = "testdata/airshipui_invalid.json"
) )
// DummyExecutableConfig returns a populated Executable struct
func dummyExecutableConfig() *Executable {
return &Executable{
AutoStart: true,
Filepath: "/fake/path/to/executable",
Args: []string{
"--fakeflag",
"fakevalue",
},
}
}
// DummyDashboardsConfig returns an array of populated Dashboard structs
func dummyDashboardsConfig() []Dashboard {
e := dummyExecutableConfig()
return []Dashboard{
{
Name: "dummy_dashboard",
BaseURL: "http://dummyhost",
Path: "fake/login/path",
Executable: e,
},
{
Name: "dummy_plugin_no_dash",
Executable: e,
},
{
Name: "dummy_dashboard_no_exe",
BaseURL: "http://dummyhost",
Path: "fake/login/path",
},
}
}
func dummyAuthMethodConfig() *AuthMethod {
return &AuthMethod{
URL: "http://fake.auth.method.com/auth",
}
}
func TestSetUIConfig(t *testing.T) { func TestSetUIConfig(t *testing.T) {
conf := configs.Config{ conf := Config{
Dashboards: testutil.DummyDashboardsConfig(), Dashboards: dummyDashboardsConfig(),
AuthMethod: testutil.DummyAuthMethodConfig(), AuthMethod: dummyAuthMethodConfig(),
} }
err := configs.SetUIConfig(testFile) err := SetUIConfig(testFile)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, conf, configs.UIConfig) assert.Equal(t, conf, UIConfig)
err = configs.SetUIConfig(invalidTestFile) err = SetUIConfig(invalidTestFile)
require.Error(t, err) require.Error(t, err)
} }
func TestFileNotFound(t *testing.T) { func TestFileNotFound(t *testing.T) {
err := configs.SetUIConfig(fakeFile) err := SetUIConfig(fakeFile)
assert.Error(t, err) assert.Error(t, err)
} }

1
internal/integrations/ctl/airshipctl.go Executable file → Normal file
View File

@ -34,7 +34,6 @@ var (
// CTLFunctionMap is a function map for the CTL functions that is referenced in the webservice // CTLFunctionMap is a function map for the CTL functions that is referenced in the webservice
var CTLFunctionMap = map[configs.WsComponentType]func(configs.WsMessage) configs.WsMessage{ var CTLFunctionMap = map[configs.WsComponentType]func(configs.WsMessage) configs.WsMessage{
configs.CTLConfig: HandleConfigRequest,
configs.Baremetal: HandleBaremetalRequest, configs.Baremetal: HandleBaremetalRequest,
configs.Document: HandleDocumentRequest, configs.Document: HandleDocumentRequest,
} }

0
internal/integrations/ctl/baremetal.go Executable file → Normal file
View File

View File

@ -18,14 +18,12 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"opendev.org/airship/airshipui/internal/configs" "opendev.org/airship/airshipui/internal/configs"
"opendev.org/airship/airshipui/util/utiltest"
) )
func TestHandleDefaultBaremetalRequest(t *testing.T) { func TestHandleDefaultBaremetalRequest(t *testing.T) {
initCTL(t) utiltest.InitConfig(t)
html, err := GetBaremetalHTML()
require.NoError(t, err)
request := configs.WsMessage{ request := configs.WsMessage{
Type: configs.AirshipCTL, Type: configs.AirshipCTL,
@ -39,10 +37,11 @@ func TestHandleDefaultBaremetalRequest(t *testing.T) {
Type: configs.AirshipCTL, Type: configs.AirshipCTL,
Component: configs.Baremetal, Component: configs.Baremetal,
SubComponent: configs.GetDefaults, SubComponent: configs.GetDefaults,
HTML: html,
} }
assert.Equal(t, expected, response) assert.Equal(t, expected.Type, response.Type)
assert.Equal(t, expected.Component, response.Component)
assert.Equal(t, expected.SubComponent, response.SubComponent)
} }
func TestHandleUnknownBaremetalSubComponent(t *testing.T) { func TestHandleUnknownBaremetalSubComponent(t *testing.T) {

View File

@ -1,180 +0,0 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package ctl
import (
"fmt"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipui/internal/configs"
)
// HandleConfigRequest will flop between requests so we don't have to have them all mapped as function calls
func HandleConfigRequest(request configs.WsMessage) configs.WsMessage {
response := configs.WsMessage{
Type: configs.AirshipCTL,
Component: configs.CTLConfig,
SubComponent: request.SubComponent,
}
var err error
var message string
switch request.SubComponent {
case configs.GetDefaults:
response.HTML, err = getConfigHTML()
case configs.SetContext:
message, err = setContext(request)
case configs.SetCluster:
message, err = setCluster(request)
case configs.SetCredential:
message, err = setCredential(request)
default:
err = fmt.Errorf("Subcomponent %s not found", request.SubComponent)
}
if err != nil {
response.Error = err.Error()
} else {
response.Message = message
}
return response
}
// GetCluster gets cluster information from the airshipctl config
func (c *Client) getCluster() []*config.Cluster {
return c.settings.Config.GetClusters()
}
// getClusterTableRows turns an array of cluster into html table rows
func getClusterTableRows() string {
info := c.getCluster()
var rows string
for _, config := range info {
// TODO: all rows are editable, probably shouldn't be
rows += "<tr><td><div contenteditable=true>" +
config.Bootstrap + "</div></td><td><div contenteditable=true>" +
config.NameInKubeconf + "</div></td><td><div contenteditable=true>" +
config.ManagementConfiguration + "</div></td><td>" +
config.KubeCluster().LocationOfOrigin + "</td><td><div contenteditable=true>" +
config.KubeCluster().Server + "</div></td><td><div contenteditable=true>" +
config.KubeCluster().CertificateAuthority + "</div></td><td>" +
"<button type=\"button\" class=\"btn btn-success\" onclick=\"saveConfig(this)\">Save</button></td></tr>"
}
return rows
}
// GetContext gets cluster information from the airshipctl config
func (c *Client) getContext() []*config.Context {
return c.settings.Config.GetContexts()
}
// getContextTableRows turns an array of contexts into html table rows
func getContextTableRows() string {
info := c.getContext()
var rows string
for _, context := range info {
// TODO: all rows are editable, probably shouldn't be
rows += "<tr><td><div contenteditable=true>" +
context.NameInKubeconf + "</div></td><td><div contenteditable=true>" +
context.Manifest + "</div></td><td>" +
context.KubeContext().LocationOfOrigin + "</td><td><div contenteditable=true>" +
context.KubeContext().Cluster + "</div></td><td><div contenteditable=true>" +
context.KubeContext().AuthInfo + "</div></td><td>" +
"<button type=\"button\" class=\"btn btn-success\" onclick=\"saveConfig(this)\">Save</button></td></tr>"
}
return rows
}
// GetCredential gets user credentials from the airshipctl config
func (c *Client) getCredential() []*config.AuthInfo {
authinfo, err := c.settings.Config.GetAuthInfos()
if err != nil {
return []*config.AuthInfo{}
}
return authinfo
}
// getContextTableRows turns an array of contexts into html table rows
func getCredentialTableRows() string {
info := c.getCredential()
var rows string
for _, credential := range info {
// TODO: all rows are editable, probably shouldn't be
rows += "<tr><td>" +
credential.KubeAuthInfo().LocationOfOrigin + "</td><td><div contenteditable=true>" +
credential.KubeAuthInfo().Username + "</div></td><td>" +
"<button type=\"button\" class=\"btn btn-success\" onclick=\"saveConfig(this)\">Save</button></td></tr>"
}
return rows
}
func getConfigHTML() (string, error) {
return getHTML("/templates/config.html", ctlPage{
ClusterRows: getClusterTableRows(),
ContextRows: getContextTableRows(),
CredentialRows: getCredentialTableRows(),
Title: "Config",
Version: getAirshipCTLVersion(),
})
}
// SetCluster will take ui cluster info, translate them into CTL commands and send a response back to the UI
func setCluster(request configs.WsMessage) (string, error) {
modified, err := config.RunSetCluster(request.ClusterOptions, c.settings.Config, true)
var message string
if modified {
message = fmt.Sprintf("Cluster %q of type %q modified.",
request.ClusterOptions.Name, request.ClusterOptions.ClusterType)
} else {
message = fmt.Sprintf("Cluster %q of type %q created.",
request.ClusterOptions.Name, request.ClusterOptions.ClusterType)
}
return message, err
}
// SetContext will take ui context info, translate them into CTL commands and send a response back to the UI
func setContext(request configs.WsMessage) (string, error) {
modified, err := config.RunSetContext(request.ContextOptions, c.settings.Config, true)
var message string
if modified {
message = fmt.Sprintf("Context %q modified.", request.ContextOptions.Name)
} else {
message = fmt.Sprintf("Context %q created.", request.ContextOptions.Name)
}
return message, err
}
// SetContext will take ui context info, translate them into CTL commands and send a response back to the UI
func setCredential(request configs.WsMessage) (string, error) {
modified, err := config.RunSetAuthInfo(request.AuthInfoOptions, c.settings.Config, true)
var message string
if modified {
message = fmt.Sprintf("Credential %q modified.", request.AuthInfoOptions.Name)
} else {
message = fmt.Sprintf("Credential %q created.", request.AuthInfoOptions.Name)
}
return message, err
}

View File

@ -1,120 +0,0 @@
/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package ctl
import (
"log"
"testing"
"opendev.org/airship/airshipctl/pkg/environment"
"opendev.org/airship/airshipui/internal/configs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"opendev.org/airship/airshipui/testutil"
)
// TODO: Determine if this should be broken out into it's own file
// setup the airshipCTL env prior to running
func initCTL(t *testing.T) {
conf, configPath, kubeConfigPath, cleanup := testutil.InitConfig(t)
defer cleanup(t)
// point airshipctl client toward test configs
c.settings = &environment.AirshipCTLSettings{
AirshipConfigPath: configPath,
KubeConfigPath: kubeConfigPath,
Config: conf,
}
err := c.settings.Config.LoadConfig(
c.settings.AirshipConfigPath,
c.settings.KubeConfigPath,
)
if err != nil {
log.Fatal(err)
}
}
func TestHandleDefaultConfigRequest(t *testing.T) {
initCTL(t)
// get the default html
html, err := getConfigHTML()
require.NoError(t, err)
// simulate incoming WsMessage from websocket client
request := configs.WsMessage{
Type: configs.AirshipCTL,
Component: configs.CTLConfig,
SubComponent: configs.GetDefaults,
}
response := HandleConfigRequest(request)
expected := configs.WsMessage{
Type: configs.AirshipCTL,
Component: configs.CTLConfig,
SubComponent: configs.GetDefaults,
HTML: html,
}
assert.Equal(t, expected, response)
request = configs.WsMessage{
Type: configs.AirshipCTL,
Component: configs.CTLConfig,
SubComponent: configs.SetCredential,
AuthInfoOptions: testutil.DummyAuthInfoOptions(),
}
response = HandleConfigRequest(request)
assert.Contains(t, response.Message, "created")
request = configs.WsMessage{
Type: configs.AirshipCTL,
Component: configs.CTLConfig,
SubComponent: configs.SetCluster,
ClusterOptions: testutil.DummyClusterOptions(),
}
response = HandleConfigRequest(request)
assert.Contains(t, response.Message, "created")
request = configs.WsMessage{
Type: configs.AirshipCTL,
Component: configs.CTLConfig,
SubComponent: configs.SetContext,
ContextOptions: testutil.DummyContextOptions(),
}
response = HandleConfigRequest(request)
assert.Contains(t, response.Message, "created")
}
func TestHandleUnknownConfigSubComponent(t *testing.T) {
request := configs.WsMessage{
Type: configs.AirshipCTL,
Component: configs.CTLConfig,
SubComponent: "fake_subcomponent",
}
response := HandleConfigRequest(request)
expected := configs.WsMessage{
Type: configs.AirshipCTL,
Component: configs.CTLConfig,
SubComponent: "fake_subcomponent",
Error: "Subcomponent fake_subcomponent not found",
}
assert.Equal(t, expected, response)
}

32
internal/integrations/ctl/document.go Executable file → Normal file
View File

@ -19,8 +19,6 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath"
"strings"
"opendev.org/airship/airshipctl/pkg/document/pull" "opendev.org/airship/airshipctl/pkg/document/pull"
"opendev.org/airship/airshipui/internal/configs" "opendev.org/airship/airshipui/internal/configs"
@ -38,7 +36,6 @@ func HandleDocumentRequest(request configs.WsMessage) configs.WsMessage {
var message string var message string
switch request.SubComponent { switch request.SubComponent {
case configs.GetDefaults: case configs.GetDefaults:
response.HTML, err = GetDocumentHTML()
response.Data = getGraphData() response.Data = getGraphData()
case configs.DocPull: case configs.DocPull:
message, err = c.docPull() message, err = c.docPull()
@ -129,32 +126,3 @@ func (c *Client) docPull() (string, error) {
return message, err return message, err
} }
// GetDocumentHTML will return the templated document pagelet
func GetDocumentHTML() (string, error) {
return getHTML("/templates/document.html", ctlPage{
Title: "Document",
Version: getAirshipCTLVersion(),
YAMLTree: getYamlTree(),
YAMLHome: filepath.Dir(c.settings.AirshipConfigPath),
})
}
// TODO: when we figure out what tree structure we're doing make this dynamic
// The string builder is unnecessary in an non dynamic role, so it may be needed later
func getYamlTree() string {
var s strings.Builder
s.WriteString("<li><table>" +
"<tr><td><span class=\"document\" id=\"AirshipConfigSpan\"> </span></td>" +
"<td><button id=\"AirshipConfigBtn\" class=\"unstyled-button\" onclick=\"return documentAction(this)\"> - " +
filepath.Base(c.settings.AirshipConfigPath) +
"</button></td></tr>" +
"<tr><td><span class=\"document\" id=\"KubeConfigSpan\"> </span></td>" +
"<td><button id=\"KubeConfigBtn\" class=\"unstyled-button\" onclick=\"return documentAction(this)\"> - " +
filepath.Base(c.settings.KubeConfigPath) +
"</button></td></tr>" +
"</table></li>")
return s.String()
}

View File

@ -18,14 +18,12 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"opendev.org/airship/airshipui/internal/configs" "opendev.org/airship/airshipui/internal/configs"
"opendev.org/airship/airshipui/util/utiltest"
) )
func TestHandleDefaultDocumentRequest(t *testing.T) { func TestHandleDefaultDocumentRequest(t *testing.T) {
initCTL(t) utiltest.InitConfig(t)
html, err := GetDocumentHTML()
require.NoError(t, err)
request := configs.WsMessage{ request := configs.WsMessage{
Type: configs.AirshipCTL, Type: configs.AirshipCTL,
@ -39,11 +37,13 @@ func TestHandleDefaultDocumentRequest(t *testing.T) {
Type: configs.AirshipCTL, Type: configs.AirshipCTL,
Component: configs.Document, Component: configs.Document,
SubComponent: configs.GetDefaults, SubComponent: configs.GetDefaults,
HTML: html,
Data: getGraphData(), Data: getGraphData(),
} }
assert.Equal(t, expected, response) assert.Equal(t, expected.Type, response.Type)
assert.Equal(t, expected.Component, response.Component)
assert.Equal(t, expected.SubComponent, response.SubComponent)
assert.Equal(t, expected.Data, response.Data)
} }
func TestHandleUnknownDocumentSubComponent(t *testing.T) { func TestHandleUnknownDocumentSubComponent(t *testing.T) {

View File

@ -1,5 +0,0 @@
<h1>Airship CTL {{.Title}} Base Information</h1>
<p>Version: {{.Version}}</p>
<h2>Generate ISO</h2>
<button type="button" class="btn btn-info" id="GenIsoBtn" onclick="baremetalAction(this)" style="width: 150px;" {{.Disabled}}>{{.ButtonText}}</button>

Some files were not shown because too many files have changed in this diff Show More