mirror of
https://github.com/optim-enterprises-bv/siembol.git
synced 2025-11-02 03:18:09 +00:00
committed by
GitHub Enterprise
parent
9f68bea6f5
commit
f5e2ea47fd
45
.gitignore
vendored
45
.gitignore
vendored
@@ -26,4 +26,47 @@ pom.xml.versionsBackup
|
||||
*.pyc
|
||||
|
||||
#Storm
|
||||
worker.yaml
|
||||
worker.yaml
|
||||
|
||||
## Angular
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/dist-test
|
||||
/tmp
|
||||
/out-tsc
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# IDEs and editors
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# e2e
|
||||
/e2e/*.js
|
||||
/e2e/*.map
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
13
config-editor/config-editor-ui/.editorconfig
Normal file
13
config-editor/config-editor-ui/.editorconfig
Normal file
@@ -0,0 +1,13 @@
|
||||
# Editor configuration, see http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
@@ -1 +1,33 @@
|
||||
"# config-editor-ui"
|
||||
# Config Editor UI
|
||||
|
||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.0.6.
|
||||
|
||||
## 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|module`.
|
||||
|
||||
## Install
|
||||
|
||||
If Angular CLI is not installed, follow instructions from: https://confluence.uberit.net/display/DEVGLO/Web+Application+Development (`Installation` section)
|
||||
Then run `npm install`
|
||||
|
||||
## 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/).
|
||||
Before running the tests make sure you are serving the app via `ng serve`.
|
||||
|
||||
## 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).
|
||||
|
||||
131
config-editor/config-editor-ui/angular.json
Normal file
131
config-editor/config-editor-ui/angular.json
Normal file
@@ -0,0 +1,131 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"ruleeditor": {
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"projectType": "application",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"assets": [
|
||||
{ "glob": "**/*", "input": "node_modules/ngx-monaco-editor/assets/monaco", "output": "./assets/monaco/" },
|
||||
"src/assets",
|
||||
"src/favicon.ico",
|
||||
"src/config.json"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "ruleeditor:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "ruleeditor:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "ruleeditor:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"karmaConfig": "./karma.conf.js",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "src/tsconfig.spec.json",
|
||||
"scripts": [],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"assets": [
|
||||
"src/assets",
|
||||
"src/favicon.ico",
|
||||
"src/config.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"src/tsconfig.app.json",
|
||||
"src/tsconfig.spec.json"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ruleeditor-e2e": {
|
||||
"root": "",
|
||||
"sourceRoot": "",
|
||||
"projectType": "application",
|
||||
"architect": {
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "./protractor.conf.js",
|
||||
"devServerTarget": "ruleeditor:serve"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"e2e/tsconfig.e2e.json"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "ruleeditor",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"prefix": "re",
|
||||
"styleext": "scss"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"prefix": "re"
|
||||
}
|
||||
}
|
||||
}
|
||||
3
config-editor/config-editor-ui/browserslist
Normal file
3
config-editor/config-editor-ui/browserslist
Normal file
@@ -0,0 +1,3 @@
|
||||
Firefox ESR # the latest [Firefox ESR] version.
|
||||
not dead # Don't support outdated browsers
|
||||
not IE 9-11 # Don't support IE 9 - 11
|
||||
44
config-editor/config-editor-ui/karma.conf.js
Normal file
44
config-editor/config-editor-ui/karma.conf.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/0.13/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
|
||||
},
|
||||
files: [
|
||||
|
||||
],
|
||||
preprocessors: {
|
||||
|
||||
},
|
||||
mime: {
|
||||
'text/x-typescript': ['ts','tsx']
|
||||
},
|
||||
coverageIstanbulReporter: {
|
||||
dir: require('path').join(__dirname, 'coverage'), reports: [ 'html', 'lcovonly' ],
|
||||
fixWebpackSourcePaths: true
|
||||
},
|
||||
angularCli: {
|
||||
environment: 'dev'
|
||||
},
|
||||
reporters: config.angularCli && config.angularCli.codeCoverage
|
||||
? ['progress', 'coverage-istanbul']
|
||||
: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false
|
||||
});
|
||||
};
|
||||
13749
config-editor/config-editor-ui/npm-shrinkwrap.json
generated
Normal file
13749
config-editor/config-editor-ui/npm-shrinkwrap.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
77
config-editor/config-editor-ui/package.json
Normal file
77
config-editor/config-editor-ui/package.json
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"name": "rule-editor.ui",
|
||||
"version": "1.1.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint --type-check",
|
||||
"e2e": "ng e2e",
|
||||
"build-prod": "ng build --prod --progress=false",
|
||||
"test-once": "ng test --single-run --code-coverage",
|
||||
"test-with-coverage": "ng test --code-coverage",
|
||||
"build-test": "webpack --config webpack.test.config.js",
|
||||
"prebuild-test": "rimraf dist-test",
|
||||
"component": "ng generate component"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular-devkit/build-angular": "^0.803.1",
|
||||
"@angular/animations": "^8.2.4",
|
||||
"@angular/cdk": "^8.1.4",
|
||||
"@angular/common": "^8.2.4",
|
||||
"@angular/compiler": "^8.2.4",
|
||||
"@angular/core": "^8.2.4",
|
||||
"@angular/flex-layout": "^8.0.0-beta.26",
|
||||
"@angular/forms": "^8.2.4",
|
||||
"@angular/material": "^8.1.4",
|
||||
"@angular/platform-browser": "^8.2.4",
|
||||
"@angular/platform-browser-dynamic": "^8.2.4",
|
||||
"@angular/router": "^8.2.4",
|
||||
"@juggle/resize-observer": "^2.3.0",
|
||||
"@ngrx/effects": "^8.3.0",
|
||||
"@ngrx/router-store": "^8.3.0",
|
||||
"@ngrx/store": "^8.3.0",
|
||||
"@ngrx/store-devtools": "^8.3.0",
|
||||
"@ngx-formly/core": "^5.4.1",
|
||||
"@ngx-formly/material": "^5.4.1",
|
||||
"@types/dragula": "^2.1.34",
|
||||
"ajv": "^6.8.1",
|
||||
"core-js": "~2.5.6",
|
||||
"diff-match-patch": "^1.0.4",
|
||||
"hammerjs": "^2.0.8",
|
||||
"json-ptr": "^1.2.0",
|
||||
"ngx-mat-select-search": "^1.2.3",
|
||||
"ngx-scrollbar": "^5.0.1",
|
||||
"omit-empty": "^1.0.0",
|
||||
"rxjs": "^6.5.2",
|
||||
"rxjs-compat": "^6.5.2",
|
||||
"zone.js": "^0.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/cli": "^8.3.1",
|
||||
"@angular/compiler-cli": "^8.2.4",
|
||||
"@types/jasmine": "2.8.7",
|
||||
"@types/node": "~10.1.2",
|
||||
"codelyzer": "~4.3.0",
|
||||
"jasmine": "^3.1.0",
|
||||
"jasmine-core": "~3.1.0",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"karma": "~2.0.2",
|
||||
"karma-chrome-launcher": "~2.2.0",
|
||||
"karma-cli": "~1.0.1",
|
||||
"karma-coverage-istanbul-reporter": "^2.0.1",
|
||||
"karma-jasmine": "~1.1.2",
|
||||
"karma-jasmine-html-reporter": "^1.1.0",
|
||||
"karma-nunit2-reporter": "^0.3.0",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"ngrx-store-freeze": "^0.2.3",
|
||||
"protractor": "~5.3.2",
|
||||
"rimraf": "^2.6.1",
|
||||
"ts-node": "^6.0.5",
|
||||
"tslint": "~5.10.0",
|
||||
"typescript": "^3.5.3"
|
||||
}
|
||||
}
|
||||
28
config-editor/config-editor-ui/protractor.conf.js
Normal file
28
config-editor/config-editor-ui/protractor.conf.js
Normal file
@@ -0,0 +1,28 @@
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./e2e/**/*.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: 'e2e/tsconfig.e2e.json'
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { InitComponent } from '@app/components/init-component/init.component';
|
||||
import { ViewResolver } from '@app/guards';
|
||||
|
||||
const appRoutes: Routes = [
|
||||
{
|
||||
component: InitComponent,
|
||||
path: '**',
|
||||
resolve: {
|
||||
message: ViewResolver,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
exports: [RouterModule],
|
||||
imports: [RouterModule.forRoot(appRoutes, { useHash: true })],
|
||||
providers: [ViewResolver],
|
||||
})
|
||||
export class AppRoutingModule {
|
||||
constructor() {}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
||||
import { RouterStateUrl } from '@app/app-routing';
|
||||
import * as fromRouter from '@ngrx/router-store';
|
||||
|
||||
/**
|
||||
* The RouterStateSerializer takes the current RouterStateSnapshot
|
||||
* and returns any pertinent information needed. The snapshot contains
|
||||
* all information about the state of the router at the given point in time.
|
||||
* The entire snapshot is complex and not always needed. In this case, you only
|
||||
* need the URL and query parameters from the snapshot in the store. Other items could be
|
||||
* returned such as route parameters and static route data.
|
||||
*
|
||||
* source: https://github.com/ngrx/platform/blob/master/docs/router-store/api.md#custom-router-state-serializer
|
||||
*/
|
||||
export class CustomRouterStateSerializer implements fromRouter.RouterStateSerializer<RouterStateUrl> {
|
||||
serialize(routerState: RouterStateSnapshot): RouterStateUrl {
|
||||
const { url } = routerState;
|
||||
const { queryParams } = routerState.root;
|
||||
|
||||
let state: ActivatedRouteSnapshot = routerState.root;
|
||||
while (state.firstChild) {
|
||||
state = state.firstChild;
|
||||
}
|
||||
const { params } = state;
|
||||
|
||||
return { url, queryParams, params };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { RouterStateUrl } from './router-state-url';
|
||||
export { CustomRouterStateSerializer } from './custom-router-state-serializer';
|
||||
export { AppRoutingModule } from './app-routing.module';
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Params } from '@angular/router';
|
||||
|
||||
export interface RouterStateUrl {
|
||||
url: string;
|
||||
queryParams: Params;
|
||||
params: Params;
|
||||
}
|
||||
7
config-editor/config-editor-ui/src/app/app.component.ts
Normal file
7
config-editor/config-editor-ui/src/app/app.component.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 're-root',
|
||||
template: '<router-outlet></router-outlet>',
|
||||
})
|
||||
export class AppComponent { }
|
||||
212
config-editor/config-editor-ui/src/app/app.module.ts
Normal file
212
config-editor/config-editor-ui/src/app/app.module.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
import { HashLocationStrategy, LocationStrategy } from '@angular/common';
|
||||
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
|
||||
import { APP_INITIALIZER, NgModule } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { GestureConfig } from '@angular/material';
|
||||
import { BrowserModule, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { AppRoutingModule, CustomRouterStateSerializer } from '@app/app-routing';
|
||||
import {
|
||||
ConfigManagerComponent, DeployDialogComponent, EditorViewComponent,
|
||||
ErrorDialogComponent, JsonViewerComponent, LandingPageComponent, NavBarComponent, SearchComponent, SideBarComponent,
|
||||
SubmitDialogComponent
|
||||
} from '@app/components';
|
||||
import { TestingDialogComponent } from '@app/components/testing-dialog/testing-dialog.component';
|
||||
import { ConfigTileComponent } from '@app/components/tile/config-tile.component';
|
||||
import { DeploymentTileComponent } from '@app/components/tile/deployment-tile.component';
|
||||
import { AppConfigService, ConfigModule } from '@app/config';
|
||||
import { HomeComponent, PageNotFoundComponent } from '@app/containers';
|
||||
import { CoreModule } from '@app/core';
|
||||
import { CredentialsInterceptor } from '@app/credentials-interceptor';
|
||||
import { RepoResolver, ViewResolver } from '@app/guards';
|
||||
import { StripSuffixPipe } from '@app/pipes';
|
||||
import { SharedModule } from '@app/shared';
|
||||
import { metaReducers, reducers } from '@app/store';
|
||||
import { EditorEffects } from '@app/store/editor.effects';
|
||||
import { RouterEffects } from '@app/store/router-effects';
|
||||
import { EffectsModule } from '@ngrx/effects';
|
||||
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
|
||||
import { StoreModule } from '@ngrx/store';
|
||||
import { FormlyModule } from '@ngx-formly/core';
|
||||
import { environment } from 'environments/environment';
|
||||
import 'hammerjs';
|
||||
import { NgxMatSelectSearchModule } from 'ngx-mat-select-search'
|
||||
import { NgScrollbarModule } from 'ngx-scrollbar';
|
||||
import { AppComponent } from './app.component';
|
||||
import { ChangeHistoryComponent } from './components/change-history/change-history.component';
|
||||
import { EditorComponent } from './components/editor/editor.component';
|
||||
import { InitComponent } from './components/init-component/init.component';
|
||||
import { ConfigStoreGuard } from './guards/config-store.guard';
|
||||
import { ArrayTypeComponent } from './ngx-formly/components/array.type';
|
||||
import { ExpansionPanelWrapperComponent } from './ngx-formly/components/expansion-panel-wrapper.component';
|
||||
import { FormFieldWrapperComponent } from './ngx-formly/components/form-field-wrapper.component';
|
||||
import { InputTypeComponent } from './ngx-formly/components/input.type.component';
|
||||
import { NullTypeComponent } from './ngx-formly/components/null.type';
|
||||
import { ObjectTypeComponent } from './ngx-formly/components/object.type.component';
|
||||
import { PanelWrapperComponent } from './ngx-formly/components/panel-wrapper.component';
|
||||
import { TabsWrapperComponent } from './ngx-formly/components/tabs-wrapper.component';
|
||||
import { TabsetTypeComponent } from './ngx-formly/components/tabset.type.component';
|
||||
import { TextAreaTypeComponent } from './ngx-formly/components/textarea.type.component';
|
||||
import { HighlightVariablesPipe } from './pipes';
|
||||
import { HoverPopoverDirective } from './popover/hover-popover.directive';
|
||||
import { PopoverRendererComponent } from './popover/popover-renderer.component';
|
||||
import { PopoverService } from './popover/popover-service';
|
||||
import { PopupService } from './popup.service';
|
||||
import { NgxTextDiffModule } from './text-diff/ngx-text-diff.module';
|
||||
|
||||
import {FormlyMaterialModule} from '@ngx-formly/material';
|
||||
|
||||
export function configServiceFactory(config: AppConfigService) {
|
||||
return () => config.loadConfig();
|
||||
}
|
||||
|
||||
export function uiMetadataServiceFactory(config: AppConfigService) {
|
||||
return () => config.loadUiMetadata();
|
||||
}
|
||||
|
||||
const PROD_PROVIDERS = [
|
||||
{ provide: APP_INITIALIZER, useFactory: configServiceFactory, deps: [AppConfigService], multi: true },
|
||||
{ provide: APP_INITIALIZER, useFactory: uiMetadataServiceFactory, deps: [AppConfigService], multi: true },
|
||||
{ provide: HTTP_INTERCEPTORS, useClass: CredentialsInterceptor, multi: true },
|
||||
];
|
||||
|
||||
const DEV_PROVIDERS = [...PROD_PROVIDERS];
|
||||
|
||||
@NgModule({
|
||||
bootstrap: [AppComponent],
|
||||
declarations: [
|
||||
AppComponent,
|
||||
HomeComponent,
|
||||
PageNotFoundComponent,
|
||||
ErrorDialogComponent,
|
||||
SideBarComponent,
|
||||
EditorViewComponent,
|
||||
NavBarComponent,
|
||||
JsonViewerComponent,
|
||||
ConfigManagerComponent,
|
||||
StripSuffixPipe,
|
||||
DeployDialogComponent,
|
||||
SubmitDialogComponent,
|
||||
LandingPageComponent,
|
||||
SearchComponent,
|
||||
TestingDialogComponent,
|
||||
ConfigTileComponent,
|
||||
DeploymentTileComponent,
|
||||
InitComponent,
|
||||
EditorComponent,
|
||||
ChangeHistoryComponent,
|
||||
PopoverRendererComponent,
|
||||
HoverPopoverDirective,
|
||||
ObjectTypeComponent,
|
||||
ArrayTypeComponent,
|
||||
NullTypeComponent,
|
||||
PanelWrapperComponent,
|
||||
TabsWrapperComponent,
|
||||
TabsetTypeComponent,
|
||||
ExpansionPanelWrapperComponent,
|
||||
TextAreaTypeComponent,
|
||||
HighlightVariablesPipe,
|
||||
InputTypeComponent,
|
||||
FormFieldWrapperComponent,
|
||||
],
|
||||
entryComponents: [
|
||||
ErrorDialogComponent,
|
||||
JsonViewerComponent,
|
||||
DeployDialogComponent,
|
||||
SubmitDialogComponent,
|
||||
TestingDialogComponent,
|
||||
LandingPageComponent,
|
||||
HomeComponent,
|
||||
ConfigManagerComponent,
|
||||
EditorViewComponent,
|
||||
PageNotFoundComponent,
|
||||
EditorComponent,
|
||||
PopoverRendererComponent,
|
||||
ChangeHistoryComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
HttpClientModule,
|
||||
SharedModule,
|
||||
ConfigModule,
|
||||
CoreModule,
|
||||
AppRoutingModule,
|
||||
NgxMatSelectSearchModule,
|
||||
DragDropModule,
|
||||
NgScrollbarModule,
|
||||
NgxTextDiffModule,
|
||||
ScrollingModule,
|
||||
FormlyModule.forRoot({
|
||||
validationMessages: [
|
||||
{ name: 'required', message: 'This field is required' },
|
||||
{ name: 'null', message: 'should be null' },
|
||||
{ name: 'minlength', message: 'Min length is' },
|
||||
{ name: 'maxlength', message: 'Max length is' },
|
||||
{ name: 'min', message: 'Min is' },
|
||||
{ name: 'max', message: 'Max is' },
|
||||
{ name: 'minItems', message: 'Min items required' },
|
||||
{ name: 'maxItems', message: 'Max items' },
|
||||
],
|
||||
types: [
|
||||
{ name: 'string', component: TextAreaTypeComponent },
|
||||
{
|
||||
name: 'number',
|
||||
component: InputTypeComponent,
|
||||
wrappers: ['form-field'],
|
||||
defaultOptions: {
|
||||
templateOptions: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'integer',
|
||||
component: InputTypeComponent,
|
||||
wrappers: ['form-field'],
|
||||
defaultOptions: {
|
||||
templateOptions: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
},
|
||||
{ name: 'boolean', extends: 'checkbox' },
|
||||
{ name: 'enum', extends: 'select' },
|
||||
{ name: 'null', component: NullTypeComponent, wrappers: ['form-field'] },
|
||||
{ name: 'array', component: ArrayTypeComponent },
|
||||
{ name: 'object', component: ObjectTypeComponent },
|
||||
{ name: 'tabs', component: TabsetTypeComponent},
|
||||
],
|
||||
wrappers: [
|
||||
{ name: 'panel', component: PanelWrapperComponent },
|
||||
{ name: 'expansion-panel', component: ExpansionPanelWrapperComponent },
|
||||
{ name: 'form-field', component: FormFieldWrapperComponent },
|
||||
],
|
||||
extras: { checkExpressionOn: 'modelChange' },
|
||||
}),
|
||||
ReactiveFormsModule,
|
||||
FormlyMaterialModule,
|
||||
|
||||
// ngrx
|
||||
StoreModule.forRoot(reducers, { metaReducers }),
|
||||
EffectsModule.forRoot([EditorEffects, RouterEffects]),
|
||||
StoreRouterConnectingModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
environment.production ? PROD_PROVIDERS : DEV_PROVIDERS,
|
||||
PopupService,
|
||||
{ provide: RouterStateSerializer, useClass: CustomRouterStateSerializer },
|
||||
{ provide: LocationStrategy, useClass: HashLocationStrategy },
|
||||
{ provide: HAMMER_GESTURE_CONFIG, useClass: GestureConfig },
|
||||
ViewResolver,
|
||||
RepoResolver,
|
||||
ConfigStoreGuard,
|
||||
HighlightVariablesPipe,
|
||||
StripSuffixPipe,
|
||||
PopoverService,
|
||||
],
|
||||
|
||||
})
|
||||
export class AppModule { }
|
||||
1
config-editor/config-editor-ui/src/app/commons/index.ts
Normal file
1
config-editor/config-editor-ui/src/app/commons/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { StatusCode } from './status-code';
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum StatusCode {
|
||||
OK = 'OK',
|
||||
CREATED = 'CREATED',
|
||||
BAD_REQUEST = 'BAD_REQUEST',
|
||||
ERROR = 'INTERNAL_SERVER_ERROR',
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="container" >
|
||||
<h2 class="title">Change History</h2>
|
||||
<table class="history-table">
|
||||
<tr *ngFor="let item of history">
|
||||
<td>{{item.date | date:'yyyy MMM dd'}}</td>
|
||||
<td>{{item.author}}</td>
|
||||
<td><span class="added"><strong>+ {{item.added}}</strong></span></td>
|
||||
<td><span class="removed"><strong> - {{item.removed}}</strong></span></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
.container {
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.added {
|
||||
color: rgb(41, 145, 41);
|
||||
}
|
||||
|
||||
.removed {
|
||||
color: #bd2020;
|
||||
}
|
||||
|
||||
.history-table {
|
||||
width: 100%;
|
||||
th {
|
||||
font-weight: 400;
|
||||
text-align: left !important;
|
||||
font-size: 1.1em;
|
||||
padding: 2px 7px;
|
||||
}
|
||||
tr:nth-child(even){
|
||||
background: #f5f5f5;
|
||||
border-radius: 2px;
|
||||
}
|
||||
td {
|
||||
padding: 5px 7px;
|
||||
}
|
||||
td:first-child(), th:first-child(){
|
||||
padding-left: 0;
|
||||
}
|
||||
td:last-child(), th:last-child(){
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { FileHistory } from '../../model/config-model';
|
||||
import { PopoverContent, PopoverRef } from '../../popover/popover-ref';
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 're-change-history',
|
||||
templateUrl: './change-history.component.html',
|
||||
styleUrls: ['./change-history.component.scss'],
|
||||
})
|
||||
export class ChangeHistoryComponent implements OnInit {
|
||||
history: FileHistory[];
|
||||
content: PopoverContent;
|
||||
|
||||
constructor(private popoverRef: PopoverRef) {
|
||||
this.history = this.popoverRef.data;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.content = this.popoverRef.content;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.popoverRef.close({id: 1});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<ng-scrollbar class="scrollbar">
|
||||
<div class="container">
|
||||
<div class="rules-container">
|
||||
<div class="container-title">
|
||||
<div class="title-info">
|
||||
<h2>Store</h2>
|
||||
<span class="item-count">{{(allConfigs$ | async).length}} {{(allConfigs$ | async).length === 1 ? 'item': 'items'}}</span>
|
||||
</div>
|
||||
<re-search [searchTerm]="searchTerm$ | async" [filterMyConfigs]="filterMyConfigs$ | async" [filterUndeployed]="filterUndeployed$ | async"
|
||||
[filterUpgradable]="filterUpgradable$ | async" (searchTermChange)="onSearch($event)" (myConfigsChange)="onFilterMine($event)"
|
||||
(undeployedConfigsChange)="onFilterUndeployed($event)" (updatedConfigsChange)="onFilterUpgradable($event)">
|
||||
</re-search>
|
||||
<div class="add-button">
|
||||
<a mat-button color="primary" title="Create Config" (click)="onClickCreate()">
|
||||
<mat-icon>add</mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<div @list cdkDropList id="store-list" [cdkDropListData]="allConfigs$ | async" [cdkDropListConnectedTo]="['deployment-list']" [cdkDropListEnterPredicate]="noReturnPredicate"
|
||||
(cdkDropListDropped)="drop($event)" class="rule-list">
|
||||
<re-config-tile *ngFor="let config of (filteredConfigs$ | async); index as i" [class.selected-rule-box]="i == (selectedConfig$ |async)"
|
||||
cdkDrag [cdkDragData]="config" [config]="config" (onEdit)="onEdit(i)" (onView)="onView(i)" (onAddToDeployment)="addToDeployment(i)">
|
||||
</re-config-tile>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider [vertical]="true"></mat-divider>
|
||||
<div class="deployment-container">
|
||||
<div class="deployment-container-title">
|
||||
<div class="title-info">
|
||||
<h2>Deployment</h2>
|
||||
<span class="item-count">{{filteredDeployment.configs.length}} {{filteredDeployment.configs.length === 1 ? 'item': 'items'}}</span>
|
||||
</div>
|
||||
<a class="history-button" [hoverPopover]="deploymentHistory">
|
||||
<mat-icon>history</mat-icon>
|
||||
</a>
|
||||
<span *ngIf="!(pullRequestPending$ | async).pull_request_pending && !(releaseStatus$ | async).submitInFlight; else prMessage">
|
||||
<button class="button" mat-button color="accent" title="Deploy Configs" (click)="onDeploy()">DEPLOY</button>
|
||||
</span>
|
||||
<ng-template #prMessage>
|
||||
<span>
|
||||
<a mat-button color="accent" *ngIf="(pullRequestPending$ | async).pull_request_url !== undefined" href="{{(pullRequestPending$ | async).pull_request_url}}"
|
||||
target="_blank">PR for release pending</a>
|
||||
<button class="button" mat-button color="accent" title="refresh PR status" (click)="onRefreshPrStatus()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
</span>
|
||||
</ng-template>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<div @list cdkDropList id="deployment-list" [cdkDropListData]="deployment.configs" [cdkDropListEnterPredicate]="duplicateItemCheck"
|
||||
class="deployment-list" (cdkDropListDropped)="drop($event)">
|
||||
<ng-container *ngIf="!deployment || !deployment.configs || deployment.configs.length === 0; else configTile">
|
||||
<div class="placeholder-box">
|
||||
<h4>drag item here to add to deployment</h4>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #configTile>
|
||||
<re-deployment-tile *ngFor="let cfg of filteredDeployment.configs; index as i" [cdkDragData]="[cfg, deployment]" cdkDrag
|
||||
[config]="cfg" (onDelete)="onRemove(i)" (onUpgrade)="upgrade(i)" (onViewDiff)="onView(i, i)"></re-deployment-tile>
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-scrollbar>
|
||||
@@ -0,0 +1,205 @@
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
padding: 10px 30px;
|
||||
min-height: 100%;
|
||||
&>mat-divider {
|
||||
margin: 0 40px;
|
||||
}
|
||||
max-width: 1800px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.rules-container,
|
||||
.deployment-container {
|
||||
&>mat-divider {
|
||||
margin: 10px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.title-info {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.item-count {
|
||||
margin-left: 10px;
|
||||
color: #868686;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
a {
|
||||
min-width: 36px;
|
||||
padding: 0;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.rules-container {
|
||||
width: 60%;
|
||||
min-width: 800px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.deployment-container {
|
||||
min-width: 300px;
|
||||
width: 30%;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.rule-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.deployment-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chip-bag{
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
.container-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: white;
|
||||
margin-bottom: -5px;
|
||||
h2 {
|
||||
width: 90px;
|
||||
max-width: 90px;
|
||||
margin: 0 10px;
|
||||
font-weight: 400;
|
||||
font-size: 1.6em;
|
||||
}
|
||||
}
|
||||
|
||||
.deployment-container-title {
|
||||
@extend .container-title;
|
||||
}
|
||||
|
||||
mat-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
mat-card-title {
|
||||
color: #83d3f1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
h4 {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 400;
|
||||
}
|
||||
.unsaved-warning {
|
||||
color: rgb(180, 180, 180);
|
||||
font-style: italic;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
}
|
||||
mat-card-subtitle {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.chip {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
padding: 2px 5px;
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
border-radius: 9999px;
|
||||
color: #111;
|
||||
background: rgb(180, 180, 180);
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
mat-card-content {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
&:hover mat-card-footer {
|
||||
opacity: 1;
|
||||
}
|
||||
mat-card-footer {
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
a {
|
||||
margin: 0 5px;
|
||||
color: rgb(180, 180, 180);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
.drag-hint {
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pr-message {
|
||||
color: orange;
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.placeholder-box {
|
||||
background-color: #222;
|
||||
border: 2px dotted white;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
h4 {
|
||||
color: white;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.cdk-drag-preview {
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 5px 5px -3px rgba(0,0,0,0.2),
|
||||
0 8px 10px 1px rgba(0,0,0,0.14),
|
||||
0 3px 14px 2px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
.cdk-drag-placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.cdk-drag-animating,
|
||||
.rule-list.cdk-drop-list-dragging re-rule-tile:not(.cdk-drag-placeholder) {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.cdk-drag-animating,
|
||||
.deployment-list.cdk-drop-list-dragging re-deployment-tile:not(.cdk-drag-placeholder) {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
re-search {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
--scrollbar-thumb-color: #868686;
|
||||
--scrollbar-thumb-hover-color: #a1a1a1;
|
||||
}
|
||||
|
||||
.history-button {
|
||||
margin: 0 0 0 auto;
|
||||
color: #868686;
|
||||
margin-top: 3px;
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
import { animate, query, stagger, style, transition, trigger } from '@angular/animations';
|
||||
import {
|
||||
CdkDrag,
|
||||
CdkDragDrop,
|
||||
CdkDropList,
|
||||
copyArrayItem,
|
||||
moveItemInArray,
|
||||
transferArrayItem,
|
||||
} from '@angular/cdk/drag-drop';
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material';
|
||||
import { ConfigData, ConfigWrapper, Deployment, PullRequestInfo, SubmitStatus } from '@app/model';
|
||||
import { PopupService } from '@app/popup.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import * as fromStore from 'app/store';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { skip, take, takeUntil } from 'rxjs/operators';
|
||||
import { DeployDialogComponent } from '../deploy-dialog/deploy-dialog.component';
|
||||
import { JsonViewerComponent } from '../json-viewer/json-viewer.component';
|
||||
import { EditorService } from '@app/editor.service';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 're-config-manager',
|
||||
styleUrls: ['./config-manager.component.scss'],
|
||||
templateUrl: './config-manager.component.html',
|
||||
animations: [
|
||||
trigger('list', [
|
||||
transition(':enter', [
|
||||
transition('* => *', []),
|
||||
query(':enter', [
|
||||
style({ opacity: 0 }),
|
||||
stagger(10, [
|
||||
style({ transform: 'scale(0.8)', opacity: 0 }),
|
||||
animate('.6s cubic-bezier(.8,-0.6,0.7,1.5)',
|
||||
style({transform: 'scale(1)', opacity: 1 })),
|
||||
]),
|
||||
], {optional: true}),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
})
|
||||
export class ConfigManagerComponent implements OnInit, OnDestroy {
|
||||
private ngUnsubscribe = new Subject();
|
||||
public allConfigs$: Observable<ConfigWrapper<ConfigData>[]>;
|
||||
public filteredConfigs$: Observable<ConfigWrapper<ConfigData>[]>;
|
||||
public deployment$: Observable<Deployment<ConfigWrapper<ConfigData>>>;
|
||||
public deployment: Deployment<ConfigWrapper<ConfigData>>;
|
||||
public configs: ConfigWrapper<ConfigData>[];
|
||||
public selectedConfig$: Observable<number>;
|
||||
public selectedConfig: number;
|
||||
public pullRequestPending$: Observable<PullRequestInfo>;
|
||||
public releaseStatus$: Observable<SubmitStatus>;
|
||||
public searchTerm$: Observable<string>;
|
||||
public filteredDeployment: Deployment<ConfigWrapper<ConfigData>>;
|
||||
public filteredDeployment$: Observable<Deployment<ConfigWrapper<ConfigData>>>;
|
||||
private filteredConfigs: ConfigWrapper<ConfigData>[];
|
||||
private serviceName: string;
|
||||
public filterMyConfigs$: Observable<boolean>;
|
||||
public filterUndeployed$: Observable<boolean>;
|
||||
public filterUpgradable$: Observable<boolean>;
|
||||
public deploymentHistory;
|
||||
|
||||
private readonly UNSAVED_ITEM_ALERT = 'Unsaved item! Item must be submitted to the store before it can be deployed!';
|
||||
private readonly PR_OPEN_MESSAGE = 'A pull request is already open';
|
||||
|
||||
constructor(private store: Store<fromStore.State>, public dialog: MatDialog, private snackbar: PopupService,
|
||||
private editorService: EditorService) {
|
||||
this.allConfigs$ = this.store.select(fromStore.getConfigs);
|
||||
this.filteredConfigs$ = this.store.select(fromStore.getConfigsFilteredBySearchTerm);
|
||||
this.selectedConfig$ = this.store.select(fromStore.getSelectedConfig);
|
||||
this.pullRequestPending$ = this.store.select(fromStore.getPullRequestPending);
|
||||
this.releaseStatus$ = this.store.select(fromStore.getSubmitReleaseStatus);
|
||||
this.deployment$ = this.store.select(fromStore.getStoredDeployment);
|
||||
this.searchTerm$ = this.store.select(fromStore.getSearchTerm);
|
||||
this.filteredDeployment$ = this.store.select(fromStore.getDeploymentFilteredBySearchTerm);
|
||||
this.filterMyConfigs$ = this.store.select(fromStore.getFilterMyConfigs);
|
||||
this.filterUndeployed$ = this.store.select(fromStore.getFilterUndeployed);
|
||||
this.filterUpgradable$ = this.store.select(fromStore.getFilterUpgradable);
|
||||
this.store.select(fromStore.getDeploymentHistory).pipe(take(1))
|
||||
.subscribe(h => this.deploymentHistory = {fileHistory: h});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.selectedConfig$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(s => this.selectedConfig = s);
|
||||
this.deployment$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(s => {
|
||||
this.deployment = cloneDeep(s);
|
||||
});
|
||||
this.allConfigs$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(r => {
|
||||
this.configs = cloneDeep(r);
|
||||
});
|
||||
this.store.select(fromStore.getServiceName).pipe(takeUntil(this.ngUnsubscribe))
|
||||
.subscribe(name => this.serviceName = name);
|
||||
|
||||
this.filteredConfigs$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(s => this.filteredConfigs = s);
|
||||
this.filteredDeployment$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(s => {
|
||||
this.filteredDeployment = cloneDeep(s);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.ngUnsubscribe.next();
|
||||
this.ngUnsubscribe.complete();
|
||||
}
|
||||
|
||||
public onSearch(searchTerm: string) {
|
||||
this.store.dispatch(new fromStore.Go({
|
||||
extras: {
|
||||
queryParamsHandling: 'merge',
|
||||
},
|
||||
path: [],
|
||||
query: { search: searchTerm },
|
||||
}));
|
||||
}
|
||||
|
||||
public upgrade(index: number) {
|
||||
const originalIndex = this.deployment.configs
|
||||
.findIndex(d => d.name === this.filteredDeployment.configs[index].name);
|
||||
const items = Object.assign(this.deployment.configs.slice(), {
|
||||
[originalIndex]: this.configs.find(c => c.name === this.deployment.configs[originalIndex].name),
|
||||
});
|
||||
this.deployment.configs = items;
|
||||
this.store.dispatch(new fromStore.UpdateDeployment(this.deployment));
|
||||
}
|
||||
|
||||
public drop(event: CdkDragDrop<ConfigWrapper<ConfigData>[]>) {
|
||||
if (event.container.id === 'deployment-list' && event.previousContainer.id === 'store-list' && !event.item.data.savedInBackend) {
|
||||
alert(this.UNSAVED_ITEM_ALERT);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.container.id === 'deployment-list') {
|
||||
if (event.previousContainer.id === 'store-list') {
|
||||
// In the case that the list has been filtered select the appropriate item to copy over
|
||||
const index = this.configs.findIndex(e => e.name === this.filteredConfigs[event.previousIndex].name);
|
||||
copyArrayItem(event.previousContainer.data,
|
||||
event.container.data,
|
||||
index,
|
||||
event.currentIndex
|
||||
);
|
||||
} else if (event.previousContainer.id === 'deployment-list') {
|
||||
const prevIndex = this.deployment.configs.findIndex(e =>
|
||||
e.name === this.filteredDeployment.configs[event.previousIndex].name);
|
||||
const currIndex = this.deployment.configs.findIndex(e =>
|
||||
e.name === this.filteredDeployment.configs[event.currentIndex].name);
|
||||
moveItemInArray(event.container.data, prevIndex, currIndex);
|
||||
}
|
||||
this.store.dispatch(new fromStore.UpdateDeployment(cloneDeep(this.deployment)));
|
||||
}
|
||||
}
|
||||
|
||||
public onView(id: number, releaseId: number = undefined) {
|
||||
this.dialog.open(JsonViewerComponent, {
|
||||
data: {
|
||||
config1: releaseId === undefined ? undefined
|
||||
: this.editorService.getLoader(this.serviceName)
|
||||
.unwrapOptionalsFromArrays(cloneDeep(this.filteredDeployment.configs[releaseId])),
|
||||
config2: this.editorService.getLoader(this.serviceName).unwrapOptionalsFromArrays(cloneDeep(this.filteredConfigs[id])),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public onEdit(id: number) {
|
||||
const index = this.configs.findIndex(r => r.name === this.filteredConfigs[id].name);
|
||||
if (index > -1) {
|
||||
this.store.dispatch(new fromStore.Go({
|
||||
path: [this.serviceName, 'edit', index],
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
public addToDeployment(id: number) {
|
||||
const item: ConfigWrapper<ConfigData> = this.configs.find(r => r.name === this.filteredConfigs[id].name);
|
||||
if (item !== undefined) {
|
||||
if (!item.savedInBackend) {
|
||||
alert(this.UNSAVED_ITEM_ALERT);
|
||||
|
||||
return;
|
||||
}
|
||||
if (this.deployment.configs.find(r => r.name === item.name) === undefined) {
|
||||
const updatedDeployment: Deployment<ConfigWrapper<ConfigData>> = cloneDeep(this.deployment);
|
||||
updatedDeployment.configs.push(item);
|
||||
this.store.dispatch(new fromStore.UpdateDeployment(updatedDeployment));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public onRemove(id: number) {
|
||||
const index = this.deployment.configs
|
||||
.findIndex(r => r.name === this.filteredDeployment.configs[id].name);
|
||||
|
||||
transferArrayItem(this.deployment.configs, [], index, 0);
|
||||
this.store.dispatch(new fromStore.UpdateDeployment(cloneDeep(this.deployment)));
|
||||
}
|
||||
|
||||
public onClickCreate() {
|
||||
this.store.dispatch(new fromStore.AddConfig({
|
||||
isNew: true,
|
||||
configData: undefined,
|
||||
savedInBackend: false,
|
||||
name: `new_entry_${this.configs.length}`,
|
||||
version: 0,
|
||||
description: 'no description',
|
||||
author: 'no author',
|
||||
}));
|
||||
}
|
||||
|
||||
public onDeploy() {
|
||||
this.store.dispatch(new fromStore.LoadPullRequestStatus());
|
||||
this.pullRequestPending$.pipe(skip(1), take(1)).subscribe(a => {
|
||||
if (!a.pull_request_pending) {
|
||||
const dialogRef = this.dialog.open(DeployDialogComponent, {
|
||||
data: cloneDeep(this.deployment),
|
||||
});
|
||||
dialogRef.afterClosed().subscribe((results: Deployment<ConfigWrapper<ConfigData>>) => {
|
||||
if (results && results.configs.length > 0) {
|
||||
if (results.deploymentVersion >= 0) {
|
||||
this.store.dispatch(new fromStore.SubmitRelease(results));
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.snackbar.openNotification(this.PR_OPEN_MESSAGE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public onFilterMine($event: boolean) {
|
||||
this.store.dispatch(new fromStore.FilterMyConfigs($event));
|
||||
}
|
||||
|
||||
public onRefreshPrStatus() {
|
||||
this.store.dispatch(new fromStore.LoadPullRequestStatus());
|
||||
}
|
||||
|
||||
public duplicateItemCheck(item: CdkDrag<ConfigWrapper<ConfigData>>, deployment: CdkDropList<ConfigWrapper<ConfigData>[]>) {
|
||||
return deployment.data.find(d => d.name === item.data.name) === undefined
|
||||
? true : false;
|
||||
}
|
||||
|
||||
public noReturnPredicate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public onFilterUpgradable($event: boolean) {
|
||||
this.store.dispatch(new fromStore.FilterUpgradable($event));
|
||||
}
|
||||
|
||||
public onFilterUndeployed($event: boolean) {
|
||||
this.store.dispatch(new fromStore.FilterUndeployed($event));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<div mat-dialog-title class="title-row">
|
||||
<mat-icon *ngIf="!validating && isValid === true">check</mat-icon>
|
||||
<mat-icon *ngIf="!validating && isValid === false">cancel</mat-icon>
|
||||
<h1>Submit Deployment</h1>
|
||||
</div>
|
||||
<div mat-dialog-content>
|
||||
<div class="validation" *ngIf="validating; else validationDone">
|
||||
<p> Validating </p>
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
</div>
|
||||
<ng-template #validationDone>
|
||||
<div *ngIf="isValid === true">
|
||||
<p>Validation successful</p>
|
||||
<p>Deploying to environment: {{ environment }}</p>
|
||||
<mat-divider></mat-divider>
|
||||
<p>Items in deployment:</p>
|
||||
<div class="rules-list">
|
||||
<ol>
|
||||
<li *ngFor="let config of deployment.configs">
|
||||
{{ config.name }} <span class="extra-info">(v{{ config.version }} - {{ config.author }})</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
</div>
|
||||
<div *ngIf="isValid === false">
|
||||
<p>Validation was unsuccessful</p>
|
||||
<p>Status code: {{ statusCode }}</p>
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Message
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<ng-template matExpansionPanelContent>
|
||||
<p class="error">{{ message }}</p>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Exception
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<ng-template matExpansionPanelContent>
|
||||
<p class="error">{{ exception || 'No exception provided - it may be wrapped in the message'}}</p>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
<mat-divider></mat-divider>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="uiMetadata.deployment.extras !== undefined">
|
||||
<form [formGroup]="form">
|
||||
<formly-form [model]="extrasData" [fields]="fields" [options]="options" [form]="form"></formly-form>
|
||||
</form>
|
||||
<button mat-raised-button color="primary" (click)="onValidate()">VALIDATE</button>
|
||||
</ng-container>
|
||||
|
||||
<div mat-dialog-actions class="button-row">
|
||||
<button mat-raised-button color="primary" *ngFor="let name of [serviceName$ |async]"
|
||||
[disabled]="!testEnabled || !isValid" (click)="onClickTest()">TEST</button>
|
||||
<button mat-raised-button color="primary" [disabled]="!isValid" (click)="onClickDeploy()">DEPLOY</button>
|
||||
<button mat-raised-button color="accent" (click)="onClickClose()">CANCEL</button>
|
||||
</div>
|
||||
@@ -0,0 +1,41 @@
|
||||
.title-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
h1 {
|
||||
margin-left: 10px;
|
||||
font-weight: 400;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
}
|
||||
|
||||
.rules-list {
|
||||
margin-left: 20px;
|
||||
max-height: 500px;
|
||||
li {
|
||||
margin: 10px 0;
|
||||
.extra-info {
|
||||
color: #bbb;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 10px 0;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
mat-divider {
|
||||
margin: 20px 0;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { UiMetadataMap } from '../../model/ui-metadata-map';
|
||||
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material';
|
||||
import { StatusCode } from '@app/commons';
|
||||
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { AppConfigService } from '@app/config';
|
||||
import { EditorService } from '@app/editor.service';
|
||||
import { ConfigData, ConfigWrapper, Deployment } from '@app/model';
|
||||
import { FormlyJsonschema } from '@app/ngx-formly/formly-json-schema.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { FormlyFieldConfig, FormlyFormOptions } from '@ngx-formly/core';
|
||||
import * as fromStore from 'app/store';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { Observable } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { TestingDialogComponent } from '../testing-dialog/testing-dialog.component';
|
||||
|
||||
@Component({
|
||||
selector: 're-deploy-dialog',
|
||||
styleUrls: ['deploy-dialog.component.scss'],
|
||||
templateUrl: 'deploy-dialog.component.html',
|
||||
})
|
||||
export class DeployDialogComponent {
|
||||
deployment: Deployment<ConfigWrapper<ConfigData>>;
|
||||
environment: string;
|
||||
|
||||
isValid = undefined;
|
||||
validating = true;
|
||||
message: string;
|
||||
exception: string;
|
||||
statusCode: string;
|
||||
serviceName$: Observable<string>;
|
||||
deploymentSchema = {};
|
||||
serviceName: string;
|
||||
uiMetadata: UiMetadataMap;
|
||||
extrasData = {};
|
||||
|
||||
testEnabled = false;
|
||||
public options: FormlyFormOptions = {formState: {}};
|
||||
|
||||
fields: FormlyFieldConfig[];
|
||||
public form: FormGroup = new FormGroup({});
|
||||
|
||||
constructor(public dialogref: MatDialogRef<DeployDialogComponent>,
|
||||
private config: AppConfigService,
|
||||
public dialog: MatDialog,
|
||||
private store: Store<fromStore.State>,
|
||||
private service: EditorService,
|
||||
private formlyJsonSchema: FormlyJsonschema,
|
||||
@Inject(MAT_DIALOG_DATA) public data: Deployment<ConfigWrapper<ConfigData>>) {
|
||||
this.store.select(fromStore.getServiceName).pipe(take(1)).subscribe(r => {
|
||||
this.validating = false;
|
||||
this.serviceName = r;
|
||||
this.uiMetadata = this.config.getUiMetadata(r);
|
||||
if (this.uiMetadata.deployment.extras !== undefined) {
|
||||
this.fields = [this.formlyJsonSchema.toFieldConfig(this.createDeploymentSchema(r))];
|
||||
} else {
|
||||
this.service.getLoader(this.serviceName).validateRelease(data).pipe(take(1))
|
||||
.subscribe(s => {
|
||||
if (s !== undefined) {
|
||||
this.statusCode = s.status_code;
|
||||
if (s.status_code !== StatusCode.OK) {
|
||||
this.message = s.attributes.message;
|
||||
this.exception = s.attributes.exception;
|
||||
}
|
||||
this.validating = false;
|
||||
this.isValid = s.status_code === StatusCode.OK ? true : false;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.testEnabled = this.uiMetadata.testing.deploymentTestEnabled;
|
||||
this.deployment = data;
|
||||
this.environment = this.config.environment;
|
||||
}
|
||||
|
||||
private createDeploymentSchema(serviceName: string): string {
|
||||
const depSchema = this.service.getLoader(serviceName).originalSchema;
|
||||
depSchema.properties[this.uiMetadata.deployment.config_array] = {};
|
||||
delete depSchema.properties[this.uiMetadata.deployment.config_array];
|
||||
delete depSchema.properties[this.uiMetadata.deployment.version];
|
||||
depSchema.required = depSchema.required.filter(element => {
|
||||
if (element !== this.uiMetadata.deployment.version && element !== this.uiMetadata.deployment.config_array) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return depSchema;
|
||||
}
|
||||
|
||||
onValidate() {
|
||||
this.deployment = {...this.deployment, ...this.extrasData};
|
||||
this.service.getLoader(this.serviceName)
|
||||
.validateRelease(this.deployment).pipe(take(1)).subscribe(s => {
|
||||
if (s !== undefined) {
|
||||
this.statusCode = s.status_code;
|
||||
if (s.status_code !== StatusCode.OK) {
|
||||
this.message = s.attributes.message;
|
||||
this.exception = s.attributes.exception;
|
||||
}
|
||||
this.validating = false;
|
||||
this.isValid = s.status_code === StatusCode.OK ? true : false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onClickDeploy() {
|
||||
const deployment = this.extrasData !== undefined
|
||||
? Object.assign(cloneDeep(this.deployment), this.extrasData)
|
||||
: this.deployment;
|
||||
this.dialogref.close(deployment);
|
||||
}
|
||||
|
||||
onClickTest() {
|
||||
this.dialog.open(TestingDialogComponent, {
|
||||
data: {
|
||||
configDto: this.deployment,
|
||||
singleConfig: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onClickClose() {
|
||||
this.dialogref.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="container">
|
||||
<re-side-bar></re-side-bar>
|
||||
<mat-divider [vertical]="true"></mat-divider>
|
||||
<div class="viewport">
|
||||
<re-generic-editor
|
||||
[testEnabled]="testEnabled" [sensorFieldsEnabled]="sensorFieldsEnabled" [editorType]="serviceName">
|
||||
</re-generic-editor>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
re-side-bar {
|
||||
min-width: 400px;
|
||||
max-width: 800px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
mat-card-content {
|
||||
max-height: 50px !important;
|
||||
overflow-y: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
}
|
||||
|
||||
.viewport {
|
||||
flex: 2;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { AppConfigService } from '../../config/app-config.service';
|
||||
|
||||
import { ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import * as fromStore from '@app/store';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 're-editor-view',
|
||||
styleUrls: ['./editor-view.component.scss'],
|
||||
templateUrl: './editor-view.component.html',
|
||||
})
|
||||
export class EditorViewComponent {
|
||||
bootstrapped$: Observable<string>;
|
||||
|
||||
testEnabled = false;
|
||||
sensorFieldsEnabled = false;
|
||||
serviceName: string;
|
||||
|
||||
constructor(private store: Store<fromStore.State>, private config: AppConfigService) {
|
||||
this.store.select(fromStore.getServiceName).pipe(take(1)).subscribe(r => {
|
||||
this.serviceName = r
|
||||
this.testEnabled = this.config.getUiMetadata(r).testing.perConfigTestEnabled;
|
||||
this.sensorFieldsEnabled = this.config.getUiMetadata(r).enableSensorFields;
|
||||
});
|
||||
this.bootstrapped$ = this.store.select(fromStore.getBootstrapped);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<ng-scrollbar class="scrollbar">
|
||||
<mat-card>
|
||||
<mat-card-title>
|
||||
<div class="rule-title" *ngIf="config.isNew; else showTitle">
|
||||
<mat-form-field>
|
||||
<input matInput placeholder="Config name" [(ngModel)]="configName" name="Config name" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<ng-template #showTitle>
|
||||
<h2>{{ (configs$ | async)[selectedIndex].name }}</h2>
|
||||
</ng-template>
|
||||
<div class="button-group">
|
||||
<button *ngIf="!config.isNew" mat-raised-button color="accent" title="Clone this config" (click)="onClone()">Clone</button>
|
||||
<button mat-raised-button color="accent" title="Test this config" (click)="onTest()" [disabled]="!testEnabled">Test config</button>
|
||||
</div>
|
||||
</mat-card-title>
|
||||
<mat-card-subtitle>
|
||||
<p>v{{ (configs$ | async)[selectedIndex].version }} by {{ (configs$ | async)[selectedIndex].author }}</p>
|
||||
</mat-card-subtitle>
|
||||
<mat-form-field *ngIf="sensorFieldsEnabled">
|
||||
<mat-label>Data Source</mat-label>
|
||||
<mat-select (selectionChange)="onSelectDataSource($event.value)" [value]="selectedSensor$ | async">
|
||||
<mat-option *ngFor="let sensor of (sensors$ | async)" [value]="sensor.sensor_name">
|
||||
{{sensor.sensor_name}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-card-content>
|
||||
<form [formGroup]="form" *ngIf="fields">
|
||||
<formly-form [model]="configData" [fields]="fields" [options]="options" [form]="form"></formly-form>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
<mat-card-actions align="end">
|
||||
<button class="submit-button" mat-raised-button color="accent" type="submit" [disabled]="!form.valid" (click)="onSubmit()">Submit</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</ng-scrollbar>
|
||||
@@ -0,0 +1,79 @@
|
||||
mat-card {
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
min-width: 800px;
|
||||
margin: 20px auto;
|
||||
mat-card-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
h2 {
|
||||
max-width: 80%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 20pt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
textarea{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: white;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.mat-raised-button {
|
||||
margin: 0 5px 0 0;
|
||||
}
|
||||
|
||||
.example {
|
||||
color: red;
|
||||
}
|
||||
|
||||
::ng-deep .show-text .mat-input-element {
|
||||
-webkit-text-fill-color: unset !important;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.rule-title {
|
||||
display: flex;
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
h2 {
|
||||
padding: 0 20px 0 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-size: 0.5em;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
::ng-deep mat-expansion-panel {
|
||||
margin: 20px 0 !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-card-header-text {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
::ng-deep material-tabs-widget>.ng-star-inserted {
|
||||
margin-top: 20px !important;
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
--scrollbar-thumb-color: #868686;
|
||||
--scrollbar-thumb-hover-color: #a1a1a1;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
margin-right: 1em;
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material';
|
||||
import { AppConfigService } from '@app/config';
|
||||
import { ConfigData, ConfigWrapper, SensorFields } from '@app/model';
|
||||
import { PopupService } from '@app/popup.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { FormlyFieldConfig, FormlyFormOptions } from '@ngx-formly/core';
|
||||
import * as fromStore from 'app/store';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import * as omitEmpty from 'omit-empty';
|
||||
import { Observable, of, Subject } from 'rxjs';
|
||||
import { take, takeUntil } from 'rxjs/operators';
|
||||
import { SubmitDialogComponent } from '..';
|
||||
import { EditorService } from '../../editor.service';
|
||||
import { UiMetadataMap } from '../../model/ui-metadata-map';
|
||||
import { FormlyJsonschema } from '../../ngx-formly/formly-json-schema.service';
|
||||
import * as JsonPointer from '../../ngx-formly/util/jsonpointer.functions';
|
||||
import { TestingDialogComponent } from '../testing-dialog/testing-dialog.component';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 're-generic-editor',
|
||||
styleUrls: ['./editor.component.scss'],
|
||||
templateUrl: './editor.component.html',
|
||||
})
|
||||
export class EditorComponent implements OnInit, OnDestroy {
|
||||
public ngUnsubscribe = new Subject();
|
||||
public configs$: Observable<ConfigWrapper<ConfigData>[]>;
|
||||
public selectedIndex$: Observable<number>;
|
||||
public schema$: Observable<any>;
|
||||
public user$: Observable<string>;
|
||||
public configName: string;
|
||||
public selectedIndex: number;
|
||||
public configs: ConfigWrapper<ConfigData>[];
|
||||
public config: ConfigWrapper<ConfigData> = undefined;
|
||||
public configData: ConfigData = {};
|
||||
public schema: any = {};
|
||||
public enabled = 0;
|
||||
public user: string;
|
||||
public sensors$: Observable<SensorFields[]> = of([]);
|
||||
public selectedSensor$: Observable<string>;
|
||||
private metaDataMap: UiMetadataMap;
|
||||
public fields: FormlyFieldConfig[];
|
||||
public options: FormlyFormOptions = {};
|
||||
public sensors: SensorFields[] = [];
|
||||
private serviceName: string;
|
||||
private dynamicFieldsMap: Map<string, string>;
|
||||
|
||||
public form: FormGroup = new FormGroup({});
|
||||
|
||||
private readonly NO_INPUT_MESSAGE = 'No data inputted to form';
|
||||
private readonly NO_NAME_MESSAGE = 'A name must be provided';
|
||||
private readonly UNIQUE_NAME_MESSAGE = 'Config name must be unique';
|
||||
private readonly SPACE_IN_NAME_MESSAGE = 'Config names cannot contain spaces';
|
||||
|
||||
@Input() testEnabled: boolean;
|
||||
@Input() sensorFieldsEnabled: boolean;
|
||||
@Input() editorType: string;
|
||||
|
||||
constructor(public store: Store<fromStore.State>, public dialog: MatDialog, public snackbar: PopupService,
|
||||
private appConfigService: AppConfigService, private formlyJsonschema: FormlyJsonschema, private editorService: EditorService) {
|
||||
this.configs$ = this.store.select(fromStore.getConfigs);
|
||||
this.selectedIndex$ = this.store.select(fromStore.getSelectedConfig);
|
||||
this.schema$ = this.store.select(fromStore.getSchema);
|
||||
this.user$ = this.store.select(fromStore.getCurrentUser);
|
||||
this.store.select(fromStore.getBootstrapped).pipe(take(1)).subscribe(s => this.serviceName = s);
|
||||
this.store.select(fromStore.getDynamicFieldsMap).pipe(takeUntil(this.ngUnsubscribe)).subscribe(s => this.dynamicFieldsMap = s);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.schema$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(
|
||||
s => {
|
||||
this.fields = [this.formlyJsonschema.toFieldConfig(s.schema)];
|
||||
});
|
||||
this.metaDataMap = this.appConfigService.getUiMetadata(this.editorType);
|
||||
|
||||
this.configs$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(r => {
|
||||
this.configs = <ConfigWrapper<ConfigData>[]>r;
|
||||
if (this.config && this.selectedIndex) {
|
||||
this.config = this.configs[this.selectedIndex];
|
||||
}
|
||||
});
|
||||
|
||||
this.store.select(fromStore.getSensorListFromDataSource).pipe(takeUntil(this.ngUnsubscribe)).subscribe(
|
||||
s => this.sensors = s
|
||||
);
|
||||
|
||||
this.selectedIndex$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(r => {
|
||||
this.store.dispatch(new fromStore.SelectDataSource(undefined));
|
||||
if (this.config && this.configData) {
|
||||
this.pushRuleUpdateToState();
|
||||
}
|
||||
this.selectedIndex = r;
|
||||
this.config = this.configs[r];
|
||||
this.configData = cloneDeep(this.config.configData) || {};
|
||||
this.configName = this.config.name;
|
||||
this.options.formState = {
|
||||
mainModel: this.configData,
|
||||
sensorFields: this.sensors,
|
||||
};
|
||||
});
|
||||
|
||||
this.user$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(u => this.user = u);
|
||||
|
||||
if (this.sensorFieldsEnabled) {
|
||||
this.sensors$ = this.store.select(fromStore.getSensorFields);
|
||||
this.store.dispatch(new fromStore.LoadCentrifugeFields());
|
||||
this.selectedSensor$ = this.store.select(fromStore.getDataSource);
|
||||
this.selectedSensor$.subscribe(s => this.options.formState.sensorFields = this.sensors)
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (this.config) {
|
||||
this.store.select(fromStore.getServiceName).pipe(take(1))
|
||||
.subscribe(serviceName => {
|
||||
if (serviceName === this.editorType) {
|
||||
this.pushRuleUpdateToState();
|
||||
}
|
||||
})
|
||||
}
|
||||
this.ngUnsubscribe.next();
|
||||
this.ngUnsubscribe.complete();
|
||||
}
|
||||
|
||||
private cleanConfigData(configData: ConfigData): ConfigData {
|
||||
let cfg = this.removeFieldsWhichShouldBeHidden(this.dynamicFieldsMap, configData);
|
||||
cfg = this.editorService.getLoader(this.serviceName).produceOrderedJson(cfg, '/');
|
||||
// recursively removes null, undefined, empty objects, empty arrays from the object
|
||||
cfg = omitEmpty(cfg);
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
pushRuleUpdateToState(): ConfigWrapper<ConfigData> {
|
||||
const cleanedConfigData = this.cleanConfigData(this.configData);
|
||||
|
||||
const configToUpdate = cloneDeep(this.config);
|
||||
configToUpdate.configData = cloneDeep(cleanedConfigData);
|
||||
if (this.config.isNew) {
|
||||
configToUpdate.configData[this.metaDataMap.name] = configToUpdate.name = this.configName;
|
||||
configToUpdate.configData[this.metaDataMap.version] = configToUpdate.version = 0;
|
||||
configToUpdate.configData[this.metaDataMap.author] = configToUpdate.author = this.user;
|
||||
} else {
|
||||
configToUpdate.configData[this.metaDataMap.name] = configToUpdate.name = this.config.name;
|
||||
configToUpdate.configData[this.metaDataMap.version] = configToUpdate.version = this.config.version;
|
||||
configToUpdate.configData[this.metaDataMap.author] = configToUpdate.author = this.config.author;
|
||||
}
|
||||
configToUpdate.description = configToUpdate.configData[this.metaDataMap.description];
|
||||
|
||||
// check if rule has been changed, mark unsaved
|
||||
if (JSON.stringify(this.config.configData) !== JSON.stringify(configToUpdate.configData)) {
|
||||
configToUpdate.savedInBackend = false;
|
||||
}
|
||||
const newConfigs = Object.assign(this.configs.slice(), {
|
||||
[this.selectedIndex]: configToUpdate,
|
||||
});
|
||||
this.store.dispatch(new fromStore.UpdateConfigs(newConfigs));
|
||||
|
||||
return configToUpdate;
|
||||
}
|
||||
|
||||
private removeFieldsWhichShouldBeHidden(functionsMap: Map<string, string>, cfg): ConfigData {
|
||||
let data = cloneDeep(cfg);
|
||||
const functionsMapKeys = [];
|
||||
const functionsMapItr = functionsMap.keys();
|
||||
for (let i = 0; i < functionsMap.size; ++i) {
|
||||
functionsMapKeys.push(functionsMapItr.next().value);
|
||||
}
|
||||
|
||||
for (const stringPath of functionsMapKeys) {
|
||||
const path = JsonPointer.JsonPointer.parse(stringPath);
|
||||
// find the lengths of arrays of objects so they can be iterated through with conditional function
|
||||
const arrayIndices = [];
|
||||
for (let j = 0; j < path.length; j++) {
|
||||
// in generic path '-' corresponds to an array index
|
||||
if (path[j] === '-') {
|
||||
const obj = JsonPointer.JsonPointer.get(data, path.slice(0, j));
|
||||
if (obj !== undefined) {
|
||||
arrayIndices.push(obj.length - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (arrayIndices.length === 0) {
|
||||
this.removeFromDataIfFuncFalse(data, {}, functionsMap, stringPath);
|
||||
}
|
||||
// TODO replace this with generic implementation where arrays of arrays is possible in JSON structure
|
||||
for (let j = 0; j <= arrayIndices[0]; j++) {
|
||||
this.removeFromDataIfFuncFalse(data, {parent: { parent: { key: j}, key: j } }, functionsMap, stringPath);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
private removeFromDataIfFuncFalse(data, field, functionsMap: Map<string, string>, stringPath: string) {
|
||||
try {
|
||||
const func = functionsMap.get(stringPath);
|
||||
const dynFunc = new Function('model', 'localfield', 'field', func);
|
||||
if (dynFunc(data, null, field)) {
|
||||
let indexedPointer;
|
||||
if (stringPath.includes('-')) {
|
||||
indexedPointer = JsonPointer.JsonPointer.toIndexedPointer(stringPath, [field.parent.parent.key]);
|
||||
} else {
|
||||
indexedPointer = stringPath;
|
||||
}
|
||||
JsonPointer.JsonPointer.remove(data, indexedPointer);
|
||||
}
|
||||
|
||||
} catch {
|
||||
console.warn('Something went wrong with condition evaluation when cleaning form', stringPath);
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
const config = this.pushRuleUpdateToState();
|
||||
|
||||
if (!config.configData) {
|
||||
this.snackbar.openNotification(this.NO_INPUT_MESSAGE);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.isNew) {
|
||||
if (this.configName === undefined || this.configName === '') {
|
||||
this.snackbar.openNotification(this.NO_NAME_MESSAGE);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.configs.find(f => config !== f && f.configData && f.name === this.configName) !== undefined) {
|
||||
this.snackbar.openNotification(this.UNIQUE_NAME_MESSAGE);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.configName.includes(' ')) {
|
||||
this.snackbar.openNotification(this.SPACE_IN_NAME_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
this.form = new FormGroup({});
|
||||
this.dialog.open(SubmitDialogComponent, {
|
||||
data: {
|
||||
...config,
|
||||
name: this.configName,
|
||||
},
|
||||
}).afterClosed().subscribe((submittedData: ConfigData) => {
|
||||
if (submittedData) {
|
||||
this.config = config;
|
||||
if (config.isNew) {
|
||||
this.store.dispatch(new fromStore.SubmitNewConfig(config));
|
||||
} else {
|
||||
this.store.dispatch(new fromStore.SubmitConfigEdit(config));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public onClone() {
|
||||
const newConfig: ConfigWrapper<ConfigData> = {
|
||||
isNew: true,
|
||||
configData: Object.assign({}, cloneDeep(this.config.configData), {
|
||||
[this.metaDataMap.name]: `${this.config.name}_clone`,
|
||||
[this.metaDataMap.version]: 0,
|
||||
}),
|
||||
savedInBackend: false,
|
||||
name: `${this.config.name}_clone`,
|
||||
author: this.user,
|
||||
version: 0,
|
||||
description: this.config.description,
|
||||
};
|
||||
|
||||
this.store.dispatch(new fromStore.AddConfig(newConfig));
|
||||
}
|
||||
|
||||
public onTest() {
|
||||
const currentConfig = this.pushRuleUpdateToState().configData;
|
||||
this.dialog.open(TestingDialogComponent, {
|
||||
data: {
|
||||
configDto: currentConfig,
|
||||
singleConfig: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public onSelectDataSource(dataSource: string) {
|
||||
this.store.dispatch(new fromStore.SelectDataSource(dataSource));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<h1 mat-dialog-title>Error Details</h1>
|
||||
<div mat-dialog-content>
|
||||
<p class="details">An error occurred when making a request. Please contact SecDev support for help.</p>
|
||||
<mat-divider></mat-divider>
|
||||
<p class="error">{{data.name}}, {{data.message}}</p>
|
||||
<p class="error">{{data.error.attributes.message}}</p>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<button mat-button class="button-layout" (click)="onClickClose()" tabindex="-1">CLOSE</button>
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
.details {
|
||||
font-family: Roboto, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
.error {
|
||||
font-family: monospace;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.button-layout {
|
||||
margin-right: 0px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
mat-divider {
|
||||
margin: 10px 0;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
|
||||
|
||||
@Component({
|
||||
selector: 're-error-dialog',
|
||||
styleUrls: ['error-dialog.component.scss'],
|
||||
templateUrl: 'error-dialog.component.html',
|
||||
})
|
||||
export class ErrorDialogComponent {
|
||||
constructor(public dialogref: MatDialogRef<ErrorDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any) {
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
onClickClose() {
|
||||
this.dialogref.close();
|
||||
}
|
||||
}
|
||||
10
config-editor/config-editor-ui/src/app/components/index.ts
Normal file
10
config-editor/config-editor-ui/src/app/components/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export { EditorViewComponent } from './editor-view/editor-view.component';
|
||||
export { SideBarComponent } from './side-bar/side-bar.component';
|
||||
export { NavBarComponent } from './nav-bar/nav-bar.component';
|
||||
export { JsonViewerComponent } from './json-viewer/json-viewer.component';
|
||||
export { ConfigManagerComponent } from './config-manager/config-manager.component';
|
||||
export { SubmitDialogComponent } from './submit-dialog/submit-dialog.component';
|
||||
export { DeployDialogComponent } from './deploy-dialog/deploy-dialog.component';
|
||||
export { ErrorDialogComponent } from './error-dialog/error-dialog.component';
|
||||
export { LandingPageComponent } from './landing-page/landing-page.component';
|
||||
export { SearchComponent } from './search/search.component';
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Component, OnDestroy } from '@angular/core';
|
||||
import { Router, Routes } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Subject } from 'rxjs';
|
||||
import { delay, filter, map, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators';
|
||||
import { ConfigManagerComponent, EditorViewComponent, LandingPageComponent } from '..';
|
||||
import { HomeComponent, PageNotFoundComponent } from '../../containers';
|
||||
import { EditorService } from '../../editor.service';
|
||||
import * as fromGuards from '../../guards';
|
||||
import { RepoResolver, ViewResolver } from '../../guards';
|
||||
import * as fromStore from '../../store';
|
||||
import { SetServiceNames } from '../../store';
|
||||
|
||||
@Component({
|
||||
template: '',
|
||||
})
|
||||
export class InitComponent implements OnDestroy {
|
||||
|
||||
private readonly specificEditorRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: ConfigManagerComponent,
|
||||
resolve: {
|
||||
message: ViewResolver,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'edit',
|
||||
redirectTo: 'edit/0',
|
||||
},
|
||||
{
|
||||
component: EditorViewComponent,
|
||||
path: 'edit/:id',
|
||||
canActivate: [fromGuards.ConfigStoreGuard],
|
||||
},
|
||||
]
|
||||
|
||||
private appRoutes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
redirectTo: 'home',
|
||||
},
|
||||
{
|
||||
path: 'home',
|
||||
component: LandingPageComponent,
|
||||
resolve: {
|
||||
message: RepoResolver,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
private ngUnsubscribe = new Subject();
|
||||
constructor(private router: Router, private store: Store<fromStore.State>,
|
||||
private service: EditorService) {
|
||||
this.service.getServiceNames().pipe(takeUntil(this.ngUnsubscribe)).subscribe((serviceList: string[]) => {
|
||||
const routes = this.appRoutes;
|
||||
serviceList.forEach(r => {
|
||||
routes.push({path: r, component: HomeComponent, children: this.specificEditorRoutes})
|
||||
});
|
||||
routes.push({
|
||||
component: PageNotFoundComponent,
|
||||
path: '**',
|
||||
});
|
||||
this.router.resetConfig(routes);
|
||||
this.store.dispatch(new SetServiceNames(serviceList));
|
||||
this.service.createLoaders();
|
||||
})
|
||||
|
||||
this.store.select(fromStore.getServiceNames).pipe(
|
||||
takeUntil(this.ngUnsubscribe),
|
||||
switchMap(_ => {
|
||||
this.router.navigate([this.router.url]);
|
||||
|
||||
return this.store.select(fromStore.getBootstrapped).pipe(
|
||||
withLatestFrom(this.store.select(fromStore.getServiceName)),
|
||||
filter(([bootstrapped, serviceName]) => bootstrapped !== undefined && bootstrapped === serviceName),
|
||||
delay(100),
|
||||
map(() => {
|
||||
this.router.navigate([this.router.url]);
|
||||
})
|
||||
)
|
||||
})
|
||||
).subscribe();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.ngUnsubscribe.next();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="viewer">
|
||||
<td-ngx-text-diff
|
||||
[left]="(leftContent === undefined ? rightContent?.configData : leftContent?.configData) | json"
|
||||
[right]="rightContent?.configData | json"
|
||||
[loading]="false"
|
||||
[format]="leftContent === undefined ? 'LineByLine' : 'SideBySide'"
|
||||
[compareRowsStyle]="{'max-height': '1000px', 'overflow': 'auto'}"
|
||||
(compareResults)="onCompareResults($event)"
|
||||
>
|
||||
</td-ngx-text-diff>
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
.viewer{
|
||||
width: 100%;
|
||||
font-size: 1.1em;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ChangeDetectionStrategy } from '@angular/core';
|
||||
import { Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
|
||||
import { ConfigData, ConfigWrapper } from '@app/model';
|
||||
|
||||
export interface DiffContent {
|
||||
leftContent: string;
|
||||
rightContent: string;
|
||||
}
|
||||
|
||||
export interface DiffContent {
|
||||
leftContent: string;
|
||||
rightContent: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 're-json-viewer',
|
||||
styleUrls: ['./json-viewer.component.scss'],
|
||||
templateUrl: './json-viewer.component.html',
|
||||
})
|
||||
export class JsonViewerComponent {
|
||||
public leftContent: ConfigWrapper<ConfigData>;
|
||||
public rightContent: ConfigWrapper<ConfigData>;
|
||||
|
||||
constructor(public dialogRef: MatDialogRef<JsonViewerComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any) {
|
||||
|
||||
this.leftContent = data.config1;
|
||||
this.rightContent = data.config2;
|
||||
}
|
||||
|
||||
onCompareResults($event) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<div class="centre-box">
|
||||
<div>
|
||||
<h1>Config Editor UI</h1>
|
||||
<div class="rulesets">
|
||||
<mat-card *ngFor="let ruleset of serviceNames$ | async" class="ruleset">
|
||||
<h2>{{ ruleset | titlecase}} </h2>
|
||||
<button mat-raised-button color="primary" [routerLink]="['/' + ruleset]">{{ ruleset | titlecase }} Manager</button>
|
||||
<ng-container *ngIf="repositoryLinks[ruleset]">
|
||||
<a mat-raised-button color="accent" [href]="repositoryLinks[ruleset].rule_store_url">Working Repo</a>
|
||||
<a mat-raised-button color="accent" [href]="repositoryLinks[ruleset].rules_release_url">Release Repo</a>
|
||||
</ng-container>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,46 @@
|
||||
.centre-box {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
min-height: 100vh;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: 400;
|
||||
margin-bottom: 50px;
|
||||
color: #eee;
|
||||
font-size: 1.5em;
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 20px auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.rulesets {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: stretch;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ruleset {
|
||||
margin: 25px;
|
||||
padding: 40px 50px;
|
||||
h2 {
|
||||
font-size: 1em;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
a {
|
||||
display: inline-block;
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .rulesets .mat-raised-button {
|
||||
font-size: 18px !important;
|
||||
line-height: 46px !important;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { RepositoryLinks } from '@app/model';
|
||||
import * as fromStore from '@app/store';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.Default,
|
||||
selector: 're-landing-page',
|
||||
styleUrls: ['./landing-page.component.scss'],
|
||||
templateUrl: './landing-page.component.html',
|
||||
})
|
||||
export class LandingPageComponent implements OnInit, OnDestroy {
|
||||
|
||||
private ngUnsubscribe = new Subject();
|
||||
serviceNames$: Observable<string[]>;
|
||||
repositoryLinks: { [name: string]: RepositoryLinks } = {};
|
||||
|
||||
constructor(private store: Store<fromStore.State>) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.serviceNames$ = this.store.select(fromStore.getServiceNames);
|
||||
this.store.select(fromStore.getRepositoryLinks).pipe(takeUntil(this.ngUnsubscribe)).subscribe(links => {
|
||||
if (links) {
|
||||
this.repositoryLinks = links.reduce((pre, cur) => ({ ...pre, [cur.rulesetName]: cur }), {});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.ngUnsubscribe.next();
|
||||
this.ngUnsubscribe.complete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<mat-toolbar>
|
||||
<mat-toolbar-row class="nav-content">
|
||||
<div>
|
||||
<img class="logo" src="/assets/grbig.png" [class.done-spin]="!(loading$ | async)">
|
||||
<h1> {{ serviceName$ | async | titlecase }} Editor</h1>
|
||||
<h1 class="env"> {{ environment | titlecase }} </h1>
|
||||
</div>
|
||||
<div>
|
||||
<button mat-button color="accent" [routerLink]="['/']">Home</button>
|
||||
<mat-button-toggle-group>
|
||||
<mat-button-toggle *ngFor="let serviceName of serviceNames$ | async" [routerLink]="['/' + serviceName]" [checked]="serviceName === (serviceName$ | async)">
|
||||
{{ serviceName | titlecase }}
|
||||
</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
<mat-icon>account_box</mat-icon>
|
||||
<h3>{{ user$ | async }}</h3>
|
||||
</div>
|
||||
</mat-toolbar-row>
|
||||
</mat-toolbar>
|
||||
<mat-progress-bar *ngIf="loading$ | async" mode="indeterminate" color="accent"></mat-progress-bar>
|
||||
@@ -0,0 +1,69 @@
|
||||
.nav-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: #eee;
|
||||
background: #212121;
|
||||
font-size: 0.8em;
|
||||
padding: 0 30px;
|
||||
&>div {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
img {
|
||||
display: block;
|
||||
height: 50px;
|
||||
animation: spin 1.2s cubic-bezier(.55,.31,.45,.69) infinite;
|
||||
margin-left: 20px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.8em;
|
||||
display: inline-block;
|
||||
margin: 0 10px 0 25px;
|
||||
}
|
||||
button {
|
||||
margin: 0 5px;
|
||||
font-size: 1em;
|
||||
}
|
||||
mat-icon {
|
||||
margin-left: 40px;
|
||||
margin-right: 2px;
|
||||
display: inline-block;
|
||||
}
|
||||
h3 {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.env {
|
||||
margin-left: 0 !important;
|
||||
color: #ff9800 !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-button-toggle-appearance-standard .mat-button-toggle-label-content {
|
||||
line-height: 36px !important;
|
||||
padding: 0 30px !important;
|
||||
}
|
||||
|
||||
::ng-deep .mat-button-toggle-checked {
|
||||
background: rgba(6, 167, 226, 0.9);
|
||||
}
|
||||
|
||||
.done-spin {
|
||||
animation-iteration-count: 1 !important;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(720deg);
|
||||
}
|
||||
}
|
||||
|
||||
mat-progress-bar {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { ChangeDetectionStrategy } from '@angular/core';
|
||||
import { AppConfigService } from '@app/config/app-config.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import * as fromStore from 'app/store';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 're-nav-bar',
|
||||
styleUrls: ['./nav-bar.component.scss'],
|
||||
templateUrl: './nav-bar.component.html',
|
||||
})
|
||||
export class NavBarComponent {
|
||||
user$: Observable<String>;
|
||||
loading$: Observable<boolean>;
|
||||
serviceName$: Observable<string>;
|
||||
serviceNames$: Observable<string[]>;
|
||||
environment: string;
|
||||
|
||||
constructor(private store: Store<fromStore.State>, private config: AppConfigService) {
|
||||
this.user$ = this.store.select(fromStore.getCurrentUser);
|
||||
this.loading$ = this.store.select(fromStore.getLoading);
|
||||
this.serviceName$ = this.store.select(fromStore.getServiceName);
|
||||
this.serviceNames$ = this.store.select(fromStore.getServiceNames);
|
||||
this.environment = this.config.environment;
|
||||
}
|
||||
|
||||
public onSelectView(view: string) {
|
||||
this.store.dispatch(new fromStore.Go({
|
||||
extras: {
|
||||
queryParamsHandling: 'merge',
|
||||
},
|
||||
path: ['id', view],
|
||||
query: {},
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="search-box">
|
||||
<mat-icon>search</mat-icon>
|
||||
<mat-form-field class="search-container" hintLabel="Search by name, author or labels">
|
||||
<input matInput #searchBox [(ngModel)]="searchTerm" (keyup)="onSearch(searchTerm)" (keyup.escape)="onClearSearch()"/>
|
||||
<button mat-button *ngIf="searchTerm" matSuffix mat-icon-button aria-label="Clear" (click)="onClearSearch()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
<span class="checkboxes">
|
||||
<mat-checkbox (change)="clickMyConfigs($event.checked)" [ngModel]="filterMyConfigs">My Edits</mat-checkbox>
|
||||
<mat-checkbox (change)="clickNotDeployed($event.checked)" [ngModel]="filterUndeployed">Undeployed</mat-checkbox>
|
||||
<mat-checkbox (change)="clickUpdatedConfigs($event.checked)" [ngModel]="filterUpgradable">Upgradable</mat-checkbox>
|
||||
</span>
|
||||
</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
.search-box {
|
||||
display: flex;
|
||||
justify-content: stretch;
|
||||
align-items: stretch;
|
||||
mat-icon {
|
||||
margin: auto;
|
||||
}
|
||||
//margin: 10px;
|
||||
}
|
||||
|
||||
mat-checkbox {
|
||||
margin: auto;
|
||||
margin-right: 20px;
|
||||
color: #b5b5b5;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.mat-checkbox-layout {
|
||||
color: #b5b5b5;
|
||||
}
|
||||
|
||||
.checkboxes {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
flex: 3;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 're-search',
|
||||
styleUrls: ['./search.component.scss'],
|
||||
templateUrl: './search.component.html',
|
||||
})
|
||||
|
||||
export class SearchComponent implements OnInit {
|
||||
@ViewChild('searchBox', { static: true }) searchBox;
|
||||
@Input() searchTerm: string;
|
||||
@Input() filterMyConfigs: boolean;
|
||||
@Input() filterUndeployed: boolean;
|
||||
@Input() filterUpgradable: boolean;
|
||||
@Output() searchTermChange: EventEmitter<string> = new EventEmitter<string>();
|
||||
@Output() myConfigsChange: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||
@Output() undeployedConfigsChange: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||
@Output() updatedConfigsChange: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||
|
||||
myConfigs = true;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.searchBox.nativeElement.focus();
|
||||
}
|
||||
|
||||
public onSearch(searchTerm: string) {
|
||||
this.searchTermChange.emit(searchTerm);
|
||||
}
|
||||
|
||||
public onClearSearch() {
|
||||
this.onSearch('');
|
||||
this.searchTerm = '';
|
||||
}
|
||||
|
||||
public clickMyConfigs($event: boolean) {
|
||||
this.myConfigsChange.emit($event);
|
||||
}
|
||||
|
||||
public clickNotDeployed($event: boolean) {
|
||||
this.undeployedConfigsChange.emit($event);
|
||||
}
|
||||
|
||||
public clickUpdatedConfigs($event: boolean) {
|
||||
this.updatedConfigsChange.emit($event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<ng-scrollbar class="scrollbar">
|
||||
<div class="back-button">
|
||||
<button mat-raised-button color="primary" (click)="onReturn()" class="back-button-text">
|
||||
<mat-icon>keyboard_arrow_left</mat-icon>Config Manager
|
||||
</button>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<re-config-tile *ngFor="let config of (configs$ | async); index as i" [selected]="i == (selectedConfig$ |async)"
|
||||
(click)="onSelect(i)" [config]="config" [disableButtons]="'True'">
|
||||
</re-config-tile>
|
||||
</ng-scrollbar>
|
||||
@@ -0,0 +1,24 @@
|
||||
.back-button {
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
::ng-deep .back-button .mat-raised-button {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
mat-divider {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
mat-card {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
--scrollbar-thumb-color: #868686;
|
||||
--scrollbar-thumb-hover-color: #a1a1a1;
|
||||
}
|
||||
|
||||
re-rule-tile {
|
||||
margin: inherit 5px;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { ConfigData, ConfigWrapper } from '@app/model';
|
||||
import { Store } from '@ngrx/store';
|
||||
import * as fromStore from 'app/store';
|
||||
import { Observable } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { AppConfigService } from '../../config';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 're-side-bar',
|
||||
styleUrls: ['./side-bar.component.scss', '../config-manager/config-manager.component.scss'],
|
||||
templateUrl: './side-bar.component.html',
|
||||
})
|
||||
export class SideBarComponent {
|
||||
public configs$: Observable<ConfigWrapper<ConfigData>[]>;
|
||||
public selectedConfig$: Observable<number>;
|
||||
private serviceName: string;
|
||||
|
||||
constructor(public config: AppConfigService, private store: Store<fromStore.State>) {
|
||||
this.configs$ = this.store.select(fromStore.getConfigs);
|
||||
this.selectedConfig$ = this.store.select(fromStore.getSelectedConfig);
|
||||
this.store.select(fromStore.getServiceName).pipe(take(1)).subscribe(name => this.serviceName = name);
|
||||
}
|
||||
|
||||
public onReturn() {
|
||||
this.store.dispatch(new fromStore.Go({
|
||||
path: [this.serviceName],
|
||||
}));
|
||||
}
|
||||
|
||||
public onSelect(index: number) {
|
||||
this.store.dispatch(new fromStore.Go({
|
||||
path: [this.serviceName, 'edit', index],
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<div mat-dialog-title class="title-row">
|
||||
<mat-icon *ngIf="!validating && isValid">check</mat-icon>
|
||||
<mat-icon *ngIf="!validating && !isValid">cancel</mat-icon>
|
||||
<h1>Submit Config</h1>
|
||||
</div>
|
||||
<div mat-dialog-content>
|
||||
<div class="validation" *ngIf="validating; else validationDone">
|
||||
<p> Validating </p>
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
</div>
|
||||
<ng-template #validationDone>
|
||||
<div *ngIf="isValid; else invalidMessage">
|
||||
<p>Config has been validated by the backend</p>
|
||||
<p> Do you wish to submit {{ config.name }} to the store? </p>
|
||||
</div>
|
||||
<ng-template #invalidMessage>
|
||||
<p>Config did not pass backend validation.</p>
|
||||
<p>Status code: {{ statusCode }}</p>
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Message
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<ng-template matExpansionPanelContent>
|
||||
<p class="error">{{ message }}</p>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
<mat-expansion-panel>
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
Exception
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<ng-template matExpansionPanelContent>
|
||||
<p class="error">{{ exception || 'No exception provided - it may be wrapped in the message'}}</p>
|
||||
</ng-template>
|
||||
</mat-expansion-panel>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
<mat-divider></mat-divider>
|
||||
|
||||
</div>
|
||||
<div mat-dialog-actions class="button-row">
|
||||
<button mat-raised-button color="primary" [disabled]="!isValid" (click)="onClickSubmit()">SUBMIT</button>
|
||||
<button mat-raised-button color="accent" (click)="onClickClose()">CANCEL</button>
|
||||
</div>
|
||||
@@ -0,0 +1,25 @@
|
||||
.title-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
h1 {
|
||||
margin-left: 10px;
|
||||
font-weight: 400;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 10px 0;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { OnInit } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
|
||||
import { StatusCode } from '@app/commons/status-code';
|
||||
import { ConfigData, ConfigWrapper, EditorResult, ExceptionInfo } from '@app/model';
|
||||
import { Store } from '@ngrx/store';
|
||||
import * as fromStore from 'app/store';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 're-submit-dialog',
|
||||
styleUrls: ['submit-dialog.component.scss'],
|
||||
templateUrl: 'submit-dialog.component.html',
|
||||
})
|
||||
export class SubmitDialogComponent implements OnInit {
|
||||
config: ConfigWrapper<ConfigData>;
|
||||
configValidity$: Observable<EditorResult<ExceptionInfo>>;
|
||||
message: string;
|
||||
exception: string;
|
||||
statusCode: string;
|
||||
validating = true;
|
||||
isValid = false;
|
||||
|
||||
constructor(public dialogref: MatDialogRef<SubmitDialogComponent>,
|
||||
private store: Store<fromStore.State>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: ConfigWrapper<ConfigData>) {
|
||||
this.configValidity$ = this.store.select(fromStore.getConfigValidity);
|
||||
this.config = data;
|
||||
this.store.dispatch(new fromStore.ValidateConfig(this.config));
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.configValidity$.subscribe(v => {
|
||||
if (v !== undefined) {
|
||||
this.statusCode = v.status_code;
|
||||
if (v.status_code !== StatusCode.OK) {
|
||||
this.message = v.attributes.message;
|
||||
this.exception = v.attributes.exception;
|
||||
}
|
||||
this.validating = false;
|
||||
this.isValid = this.statusCode === StatusCode.OK;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onClickSubmit() {
|
||||
this.dialogref.close(this.config);
|
||||
}
|
||||
|
||||
onClickClose() {
|
||||
this.dialogref.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<div mat-dialog-title>
|
||||
<div *ngIf="!isSingleConfig">
|
||||
<h1>Test Deployment Configuration:</h1>
|
||||
<div class="title">
|
||||
<div *ngFor="let r of deploymentConfig.configs"><h3 class="list-item"> {{r?.name}} </h3></div>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="isSingleConfig">
|
||||
<h1>Test Config</h1>
|
||||
<div class="title">
|
||||
<h3 class="list-item"> {{deploymentConfig?.name}} </h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div mat-dialog-content>
|
||||
<div class="columns">
|
||||
<div class="input">
|
||||
<span class="title">
|
||||
<h4><mat-icon class="left-icon">note</mat-icon>Event</h4>
|
||||
<h4 class="help"><mat-icon [matTooltip]="EVENT_HELP">help</mat-icon></h4>
|
||||
</span>
|
||||
<textarea class="text-file" [(ngModel)]="alert" (keydown.Tab)="onTab($event)"></textarea>
|
||||
</div>
|
||||
<div class="console">
|
||||
<span class="title">
|
||||
<h4><mat-icon class="left-icon">computer</mat-icon>Test Output</h4>
|
||||
|
||||
<div *ngIf="testSuccess === 'fail'">
|
||||
<h4 class="help"><mat-icon>cancel</mat-icon></h4>
|
||||
</div>
|
||||
<div *ngIf="testSuccess === 'success'">
|
||||
<h4 class="help"><mat-icon>check</mat-icon></h4>
|
||||
</div>
|
||||
</span>
|
||||
<pre>{{ output }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div mat-dialog-actions class="button-row">
|
||||
<button mat-raised-button color="primary" (click)="beginTest()">Test Rule</button>
|
||||
<button mat-raised-button (click)="onClickClose()">Close</button>
|
||||
</div>
|
||||
@@ -0,0 +1,91 @@
|
||||
pre {
|
||||
max-width: 800px;
|
||||
min-width: 800px;
|
||||
min-height: 600px;
|
||||
max-height: 600px;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
background-color: black;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-width: 300px;
|
||||
max-width: 800px;
|
||||
min-height: 600px;
|
||||
max-height: 600px;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
background-color: black;
|
||||
color: inherit;
|
||||
border-radius: 5px;
|
||||
overflow-wrap: inherit;
|
||||
width: 800px;
|
||||
}
|
||||
|
||||
.input {
|
||||
display: block;
|
||||
margin: 20px;
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.console {
|
||||
display: block;
|
||||
margin: 20px;
|
||||
border: 1px solid rgba(255,255,255,0.3);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-weight: 400;
|
||||
font-family: monospace;
|
||||
font-size: 1.2em;
|
||||
margin: 5px;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
.columns {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.config-options {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.help {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.left-icon {
|
||||
margin: 0 10px 0 5px;
|
||||
}
|
||||
|
||||
::ng-deep .mat-tooltip {
|
||||
white-space: pre-line;
|
||||
font-size: 0.7em;
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
padding: 10px;
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { Inject } from '@angular/core';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material';
|
||||
import { AppConfigService } from '@app/config';
|
||||
import { EditorService } from '@app/editor.service';
|
||||
import { ConfigData, Deployment, EditorResult } from '@app/model';
|
||||
import { PopupService } from '@app/popup.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import * as fromStore from 'app/store';
|
||||
import { of, Subject } from 'rxjs';
|
||||
import { take, takeUntil } from 'rxjs/operators';
|
||||
import { ConfigLoaderService } from '../../config-loader.service';
|
||||
import { ConfigTestResult } from '../../model/config-model';
|
||||
|
||||
@Component({
|
||||
selector: 're-testing-dialog',
|
||||
styleUrls: ['testing-dialog.component.scss'],
|
||||
templateUrl: 'testing-dialog.component.html',
|
||||
})
|
||||
export class TestingDialogComponent implements OnDestroy, OnInit {
|
||||
deploymentConfig: Deployment<ConfigData>;
|
||||
alert: string;
|
||||
output: string;
|
||||
EVENT_HELP: string;
|
||||
testSuccess = 'none';
|
||||
private ngUnsubscribe = new Subject();
|
||||
private service: ConfigLoaderService;
|
||||
private env: string;
|
||||
isSingleConfig: boolean;
|
||||
|
||||
constructor(public dialogref: MatDialogRef<TestingDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private editorService: EditorService,
|
||||
public snackbar: PopupService,
|
||||
private store: Store<fromStore.State>,
|
||||
private appConfig: AppConfigService) {
|
||||
this.store.select(fromStore.getServiceName).pipe(take(1)).subscribe(r => {
|
||||
this.env = r;
|
||||
this.service = this.editorService.getLoader(r);
|
||||
this.EVENT_HELP = this.appConfig.getUiMetadata(this.env).testing.helpMessage;
|
||||
})
|
||||
this.deploymentConfig = data.configDto;
|
||||
this.isSingleConfig = data.singleConfig;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.store.select(fromStore.getConfigTestingEvent).pipe(take(1)).subscribe(e => this.alert = e);
|
||||
}
|
||||
|
||||
public beginTest() {
|
||||
this.testSuccess = 'none';
|
||||
if (this.alert === null || this.alert === undefined || this.alert === '') {
|
||||
this.output = 'Error: please provide an alert to test the rule against';
|
||||
this.testSuccess = 'fail';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let event: any;
|
||||
try {
|
||||
// TODO remove this once test specification is done properly
|
||||
if (this.env === 'parserconfig') {
|
||||
event = {log: this.alert};
|
||||
} else {
|
||||
event = JSON.parse(this.alert);
|
||||
}
|
||||
} catch (e) {
|
||||
this.output = 'Error in alert JSON:\n\n' + e;
|
||||
this.testSuccess = 'fail';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const testDto: any = {
|
||||
files: [{
|
||||
content:
|
||||
this.deploymentConfig,
|
||||
}],
|
||||
event: event,
|
||||
};
|
||||
|
||||
if (this.service) {
|
||||
if (this.isSingleConfig) {
|
||||
this.service.testSingleConfig(testDto).pipe(takeUntil(this.ngUnsubscribe))
|
||||
.map(r => r)
|
||||
.catch(err => {
|
||||
this.ngUnsubscribe.next();
|
||||
this.snackbar.openSnackBar(err, `Error could not contact ${this.env} backend`);
|
||||
|
||||
return of(null);
|
||||
}).subscribe((b: EditorResult<ConfigTestResult>) => {
|
||||
if (b === null) {
|
||||
return;
|
||||
}
|
||||
if (b.status_code === 'OK') {
|
||||
this.output = b.attributes.test_result_output;
|
||||
} else {
|
||||
if (b.attributes.message && b.attributes.exception) {
|
||||
this.output = b.attributes.message + '\n' + b.attributes.exception;
|
||||
} else if (b.attributes.message !== undefined) {
|
||||
this.output = b.attributes.message;
|
||||
} else if (b.attributes.exception !== undefined) {
|
||||
this.output = b.attributes.exception;
|
||||
}
|
||||
}
|
||||
this.testSuccess = b.attributes.test_result_complete ? 'success' : 'fail';
|
||||
}
|
||||
)
|
||||
} else {
|
||||
this.service.testDeploymentConfig(testDto).pipe(takeUntil(this.ngUnsubscribe))
|
||||
.map(r => r)
|
||||
.catch(err => {
|
||||
this.ngUnsubscribe.next();
|
||||
this.snackbar.openSnackBar(err, `Error could not contact ${this.env} backend`);
|
||||
|
||||
return of(null);
|
||||
}).subscribe((b: EditorResult<ConfigTestResult>) => {
|
||||
if (b === null) {
|
||||
return;
|
||||
}
|
||||
if (b.status_code === 'OK') {
|
||||
this.output = b.attributes.test_result_output;
|
||||
} else {
|
||||
if (b.attributes.message && b.attributes.exception) {
|
||||
this.output = b.attributes.message + '\n' + b.attributes.exception;
|
||||
} else if (b.attributes.message !== undefined) {
|
||||
this.output = b.attributes.message;
|
||||
} else if (b.attributes.exception !== undefined) {
|
||||
this.output = b.attributes.exception;
|
||||
}
|
||||
}
|
||||
this.testSuccess = b.attributes.test_result_complete ? 'success' : 'fail';
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
this.snackbar.openSnackBar(`Error, could not load ${this.env} service correctly,
|
||||
this is likely due to not being able to fetch the service name`, 'Error loading testing service');
|
||||
}
|
||||
}
|
||||
|
||||
public onTab($event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
onClickClose() {
|
||||
this.dialogref.close();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.store.dispatch(new fromStore.StoreConfigTestingEvent(this.alert));
|
||||
this.ngUnsubscribe.next();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<div class="box" [class.selected-rule-box]="selected">
|
||||
<div class="inline">
|
||||
<div class="column-fixed" [hoverPopover]="config">
|
||||
<span class="chip-bag">
|
||||
<span #versionChip class="chip" [ngClass]="{'warn': !config.savedInBackend}">v{{config.version || '0' }}</span>
|
||||
</span>
|
||||
<h4 class="author">{{config.author}}</h4>
|
||||
</div>
|
||||
<mat-divider [vertical]="true"></mat-divider>
|
||||
<span class="column">
|
||||
<div class="inline-title">
|
||||
<h3 class="rule-title">{{config.name}}</h3>
|
||||
<span class="tags-column">
|
||||
<div class="tag-chip" [matTooltip]="tag" *ngFor="let tag of config?.tags">
|
||||
<div class="tag-text" *ngIf="tag !== null || tag !== ''">
|
||||
{{tag}}
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
<h4 class="subtitle">{{config.description}}</h4>
|
||||
</span>
|
||||
<span class="buttons" *ngIf="!disableButtons">
|
||||
<a (click)="editConfig()" [title]="'Edit Config'">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</a>
|
||||
<a (click)="viewConfig()" [title]="'View Json'">
|
||||
<mat-icon>pageview</mat-icon>
|
||||
</a>
|
||||
<a (click)="addToDeployment()" [title]="'Add to Deployment'">
|
||||
<mat-icon>arrow_forward</mat-icon>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,167 @@
|
||||
.chip {
|
||||
display: inline-block;
|
||||
margin: 0 auto;
|
||||
padding: 2px 5px;
|
||||
min-width: 40px;
|
||||
text-align: center;
|
||||
border-radius: 9999px;
|
||||
color: #111;
|
||||
background: rgb(180, 180, 180);
|
||||
font-family: monospace;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tag-chip {
|
||||
display: inline-block;
|
||||
margin: 0 auto;
|
||||
padding: 2px 5px;
|
||||
min-width: 40px;
|
||||
max-width: 200px;
|
||||
text-align: center;
|
||||
border-radius: 9999px;
|
||||
color: #111;
|
||||
background: #868686;
|
||||
font-family: monospace;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.tag-text {
|
||||
padding-top: 2px;
|
||||
font-size: 8pt;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.inline {
|
||||
display: flex;
|
||||
}
|
||||
.inline-title {
|
||||
display: flex;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.column-fixed {
|
||||
margin: 0 10px;
|
||||
width: 60px;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
mat-divider {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.column {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tags-column {
|
||||
overflow: hidden;
|
||||
margin-left: 20px;
|
||||
display: inline-flex;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.warn {
|
||||
background: orange !important;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: #83d3f1;
|
||||
font-size: 18px;
|
||||
font-weight: 200;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.rule-title {
|
||||
color: #83d3f1;
|
||||
font-size: 18px;
|
||||
font-weight: 200;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
padding-top: 5px;
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.space {
|
||||
flex: 5;
|
||||
}
|
||||
|
||||
.author {
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: 100;
|
||||
flex: 1;
|
||||
padding-top: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-weight: 100;
|
||||
padding-top: 3px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 12px;
|
||||
flex: 5;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.box {
|
||||
background-color: rgb(68, 68, 68);
|
||||
color: inherit;
|
||||
padding: 5px;
|
||||
box-shadow: 0px 2px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgba(0, 0, 0, 0.12);
|
||||
border-radius: 5px;
|
||||
margin-bottom: 5px
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-right: 0px;
|
||||
margin-left: auto;
|
||||
opacity: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
a {
|
||||
margin: 0 5px;
|
||||
color: rgb(180, 180, 180);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.drag-hint {
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
|
||||
:hover .buttons {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.chip-bag {
|
||||
margin-top: 5px;
|
||||
display: flex;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
button {
|
||||
line-height: 18px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.selected-rule-box {
|
||||
@extend .box;
|
||||
box-shadow: 0 0 0 1px rgba(255,255,255,0.9) inset !important;
|
||||
margin: 20px 0;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
import { ConfigData, ConfigWrapper } from '../../model/config-model';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 're-config-tile',
|
||||
styleUrls: ['./config-tile.component.scss'],
|
||||
templateUrl: './config-tile.component.html',
|
||||
})
|
||||
export class ConfigTileComponent {
|
||||
|
||||
@Input() config: ConfigWrapper<ConfigData>;
|
||||
@Input() disableButtons: boolean;
|
||||
@Input() selected: boolean;
|
||||
|
||||
@Output() onEdit = new EventEmitter<number>();
|
||||
@Output() onView = new EventEmitter<number>();
|
||||
@Output() onAddToDeployment = new EventEmitter<number>();
|
||||
|
||||
constructor() {}
|
||||
|
||||
editConfig() {
|
||||
this.onEdit.emit();
|
||||
}
|
||||
|
||||
viewConfig() {
|
||||
this.onView.emit();
|
||||
}
|
||||
|
||||
addToDeployment() {
|
||||
this.onAddToDeployment.emit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<div class="box">
|
||||
<div class="inline">
|
||||
<div class="column-fixed">
|
||||
<span class="chip-bag">
|
||||
<span class="chip">v{{config.version || '0' }}</span>
|
||||
</span>
|
||||
<h4 class="author">{{config.author}}</h4>
|
||||
</div>
|
||||
<mat-divider [vertical]="true"></mat-divider>
|
||||
<span class="column">
|
||||
<h3 class="title">{{config.name}}</h3>
|
||||
<button *ngIf="config.versionFlag > 0" mat-button color="accent" (click)="viewDiff()">View Diff</button>
|
||||
<button *ngIf="config.versionFlag > 0" mat-button color="accent" (click)="upgradeConfig()">Upgrade to v{{config.versionFlag}}</button>
|
||||
</span>
|
||||
<span class="buttons">
|
||||
<a (click)="deleteConfig()" [title]="'Remove from Deployment'">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</a>
|
||||
<a [title]="'Reorder'" class="drag-hint">
|
||||
<mat-icon>swap_vert</mat-icon>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ConfigData, ConfigWrapper } from '../../model/config-model';
|
||||
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 're-deployment-tile',
|
||||
styleUrls: ['./config-tile.component.scss'],
|
||||
templateUrl: './deployment-tile.component.html',
|
||||
})
|
||||
export class DeploymentTileComponent {
|
||||
|
||||
@Input() config: ConfigWrapper<ConfigData>;
|
||||
|
||||
@Output() onDelete = new EventEmitter<number>();
|
||||
@Output() onUpgrade = new EventEmitter<number>();
|
||||
@Output() onViewDiff = new EventEmitter<any>();
|
||||
|
||||
constructor() {}
|
||||
|
||||
deleteConfig() {
|
||||
this.onDelete.emit();
|
||||
}
|
||||
|
||||
upgradeConfig() {
|
||||
this.onUpgrade.emit();
|
||||
}
|
||||
|
||||
viewDiff() {
|
||||
this.onViewDiff.emit();
|
||||
}
|
||||
}
|
||||
345
config-editor/config-editor-ui/src/app/config-loader.service.ts
Normal file
345
config-editor/config-editor-ui/src/app/config-loader.service.ts
Normal file
@@ -0,0 +1,345 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AppConfigService } from './config/app-config.service';
|
||||
import { IConfigLoaderService } from './editor.service';
|
||||
import { SchemaDto } from './model';
|
||||
import {
|
||||
ConfigData,
|
||||
ConfigTestDto,
|
||||
ConfigTestResult,
|
||||
ConfigWrapper,
|
||||
ContentRuleFile,
|
||||
Deployment,
|
||||
DeploymentWrapper,
|
||||
EditorResult,
|
||||
ExceptionInfo,
|
||||
GitFiles,
|
||||
PullRequestInfo,
|
||||
RepositoryLinks,
|
||||
RepositoryLinksWrapper,
|
||||
SchemaInfo,
|
||||
} from './model/config-model';
|
||||
import { Field } from './model/sensor-fields';
|
||||
import { UiMetadataMap } from './model/ui-metadata-map';
|
||||
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
export class ConfigLoaderService implements IConfigLoaderService {
|
||||
private optionalObjects: string[] = [];
|
||||
private readonly uiMetadata: UiMetadataMap;
|
||||
private labelsFunc: Function;
|
||||
public originalSchema;
|
||||
public modelOrder = {};
|
||||
|
||||
constructor(private http: HttpClient, private config: AppConfigService, private env: string) {
|
||||
this.uiMetadata = this.config.getUiMetadata(this.env);
|
||||
try {
|
||||
this.labelsFunc = new Function('model', this.uiMetadata.labelsFunc);
|
||||
} catch {
|
||||
console.error('unable to parse labels funciton');
|
||||
this.labelsFunc = () => [];
|
||||
}
|
||||
}
|
||||
|
||||
public getConfigs(): Observable<ConfigWrapper<ConfigData>[]> {
|
||||
|
||||
return this.http.get<EditorResult<GitFiles<any>>>(
|
||||
`${this.config.serviceRoot}api/v1/${this.env}/configstore/configs`)
|
||||
.map(result => {
|
||||
if (result.attributes && result.attributes.files && result.attributes.files.length > 0) {
|
||||
return result.attributes.files.map(file => ({
|
||||
isNew: false,
|
||||
configData: this.wrapOptionalsInArray(file.content),
|
||||
savedInBackend: true,
|
||||
name: file.content[this.uiMetadata.name],
|
||||
description: file.content[this.uiMetadata.description],
|
||||
author: file.content[this.uiMetadata.author],
|
||||
version: file.content[this.uiMetadata.version],
|
||||
versionFlag: -1,
|
||||
isDeployed: false,
|
||||
tags: this.labelsFunc(file.content),
|
||||
fileHistory: file.file_history,
|
||||
}));
|
||||
}
|
||||
|
||||
throw new DOMException('bad format response when loading configs');
|
||||
});
|
||||
}
|
||||
|
||||
public getConfigsFromFiles(files: ContentRuleFile<any>[]): ConfigWrapper<ConfigData>[] {
|
||||
const ret: ConfigWrapper<ConfigData>[] = [];
|
||||
for (const file of files) {
|
||||
ret.push({
|
||||
isNew: false,
|
||||
configData: this.wrapOptionalsInArray(file.content),
|
||||
savedInBackend: true,
|
||||
name: file.content[this.uiMetadata.name],
|
||||
description: file.content[this.uiMetadata.description],
|
||||
author: file.content[this.uiMetadata.author],
|
||||
version: file.content[this.uiMetadata.version],
|
||||
versionFlag: -1,
|
||||
isDeployed: false,
|
||||
tags: this.labelsFunc(file.content),
|
||||
});
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private returnSubTree(tree, path: string): any {
|
||||
let subtree = cloneDeep(tree);
|
||||
path.split('.').forEach(node => {
|
||||
subtree = subtree[node];
|
||||
});
|
||||
|
||||
return subtree;
|
||||
}
|
||||
|
||||
public getSchema(): Observable<SchemaDto> {
|
||||
return this.http.get<EditorResult<SchemaInfo>>(`${this.config.serviceRoot}api/v1/${this.env}/configs/schema`)
|
||||
.map(x => {
|
||||
this.originalSchema = x.attributes.rules_schema;
|
||||
try {
|
||||
return this.returnSubTree(x, this.uiMetadata.perConfigSchemaPath);
|
||||
} catch {
|
||||
throw new Error('Call to schema endpoint didn\'t return the expected schema');
|
||||
}
|
||||
})
|
||||
.map(schema => {
|
||||
this.optionalObjects = []; // clear optional objects in case they have been set previously;
|
||||
this.modelOrder = {}
|
||||
this.wrapOptionalsInSchema(schema, '', '');
|
||||
delete schema.properties[this.uiMetadata.name];
|
||||
delete schema.properties[this.uiMetadata.author];
|
||||
delete schema.properties[this.uiMetadata.version];
|
||||
schema.required = schema.required.filter(
|
||||
f => (f !== this.uiMetadata.name) && (f !== this.uiMetadata.author) && (f !== this.uiMetadata.version));
|
||||
|
||||
return { schema };
|
||||
});
|
||||
}
|
||||
|
||||
// function to go through the output json and reorder the properties such that it is consistent with the schema
|
||||
public produceOrderedJson(configData: ConfigData, path: string) {
|
||||
if (this.modelOrder[path]) {
|
||||
const currentCfg = cloneDeep(configData)
|
||||
configData = {};
|
||||
for (const key of this.modelOrder[path]) {
|
||||
configData[key] = currentCfg[key];
|
||||
const searchPath = path === '/' ? path + key : path + '/' + key;
|
||||
// ensure it has children
|
||||
if (typeof(configData[key]) === typeof({}) && this.modelOrder[searchPath] !== undefined) {
|
||||
if (configData[key].length === undefined) {
|
||||
// is an object
|
||||
const tempCopy = cloneDeep(configData[key])
|
||||
configData[key] = {};
|
||||
const tmpObj = {}
|
||||
for (const orderedKey of this.modelOrder[searchPath]) {
|
||||
if (tempCopy[orderedKey] !== undefined) {
|
||||
tmpObj[orderedKey] = tempCopy[orderedKey];
|
||||
}
|
||||
}
|
||||
configData[key] = tmpObj;
|
||||
configData[key] = this.produceOrderedJson(configData[key], searchPath)
|
||||
} else {
|
||||
// is an array
|
||||
const tmp = cloneDeep(configData[key]);
|
||||
configData[key] = [];
|
||||
for (let i = 0; i < tmp.length; ++i) {
|
||||
configData[key].push(this.produceOrderedJson(tmp[i], searchPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return configData;
|
||||
}
|
||||
|
||||
private wrapOptionalsInArray(obj: object) {
|
||||
for (const optional of this.optionalObjects) {
|
||||
this.findAndWrap(obj, optional);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
private findAndWrap(obj: any, optionalKey: string) {
|
||||
if (typeof(obj) === typeof ({})) {
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (key === optionalKey) {
|
||||
obj[key] = [obj[key]];
|
||||
|
||||
return;
|
||||
}
|
||||
this.findAndWrap(obj[key], optionalKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private wrapOptionalsInSchema(obj: any, propKey?: string, path?: string): any {
|
||||
if (obj === undefined || obj === null || typeof (obj) !== typeof ({})) {
|
||||
return;
|
||||
}
|
||||
if (obj.type === 'object' && typeof(obj.properties) === typeof ({})) {
|
||||
path = path.endsWith('/') ? path + propKey : path + '/' + propKey;
|
||||
const requiredProperties = obj.required || [];
|
||||
const props = Object.keys(obj.properties);
|
||||
this.modelOrder[path] = props;
|
||||
for (const property of props) {
|
||||
const thingy = obj.properties[property];
|
||||
const isRequired = requiredProperties.includes(property);
|
||||
const isObject = thingy.type === 'object';
|
||||
if (!isRequired && isObject) {
|
||||
this.optionalObjects.push(property);
|
||||
const sub = {...thingy};
|
||||
thingy.type = 'array';
|
||||
delete thingy.required;
|
||||
delete thingy.properties;
|
||||
delete thingy.title;
|
||||
delete thingy.description;
|
||||
if (sub['x-schema-form'] !== undefined) {
|
||||
delete sub['x-schema-form'];
|
||||
}
|
||||
thingy.items = sub;
|
||||
thingy.maxItems = 1;
|
||||
this.wrapOptionalsInSchema(thingy.items, property, path);
|
||||
} else {
|
||||
this.wrapOptionalsInSchema(thingy, property, path);
|
||||
}
|
||||
}
|
||||
} else if (obj.type === 'array') {
|
||||
path = path === '/' ? path : path + '/';
|
||||
if (obj.items.type === 'object') {
|
||||
this.wrapOptionalsInSchema(obj.items, propKey, path);
|
||||
}
|
||||
} else if (obj.type === undefined && !obj.hasOwnProperty('properties')) {
|
||||
path = path === '/' ? path + propKey : path + '/' + propKey;
|
||||
for (const key of Object.keys(obj)) {
|
||||
this.wrapOptionalsInSchema(obj[key], key, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public unwrapOptionalsFromArrays(obj: any) {
|
||||
if (obj === undefined || obj === null || typeof (obj) !== typeof ({})) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (this.optionalObjects.includes(key)) {
|
||||
obj[key] = obj[key] === [] || obj[key] === undefined || obj[key] === null ? undefined : obj[key][0];
|
||||
}
|
||||
}
|
||||
for (const key of Object.keys(obj)) {
|
||||
this.unwrapOptionalsFromArrays(obj[key]);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
public getPullRequestStatus(): Observable<PullRequestInfo> {
|
||||
return this.http.get<EditorResult<PullRequestInfo>>(`${this.config.serviceRoot}api/v1/${this.env}/configstore/release/status`)
|
||||
.map(result => result.attributes);
|
||||
}
|
||||
|
||||
public getRelease(): Observable<DeploymentWrapper> {
|
||||
return this.http.get<EditorResult<GitFiles<any>>>
|
||||
(`${this.config.serviceRoot}api/v1/${this.env}/configstore/release`)
|
||||
.map(result => result.attributes.files[0])
|
||||
.map(result => (
|
||||
{
|
||||
deploymentHistory: result.file_history,
|
||||
storedDeployment: {
|
||||
deploymentVersion: result.content[this.uiMetadata.deployment.version],
|
||||
configs: result.content[this.uiMetadata.deployment.config_array].map(configData => ({
|
||||
isNew: false,
|
||||
configData: this.wrapOptionalsInArray(configData),
|
||||
savedInBackend: true,
|
||||
name: configData[this.uiMetadata.name],
|
||||
description: configData[this.uiMetadata.description],
|
||||
author: configData[this.uiMetadata.author],
|
||||
version: configData[this.uiMetadata.version],
|
||||
versionFlag: -1,
|
||||
tags: this.labelsFunc(configData),
|
||||
})),
|
||||
},
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
public getRepositoryLinks(): Observable<RepositoryLinks> {
|
||||
return this.http.get<EditorResult<RepositoryLinksWrapper>>(
|
||||
`${this.config.serviceRoot}api/v1/${this.env}/configstore/repositories`)
|
||||
.map(result => ({
|
||||
...result.attributes.rules_repositories,
|
||||
rulesetName: this.env,
|
||||
}))
|
||||
}
|
||||
|
||||
public validateConfig(config: ConfigWrapper<ConfigData>): Observable<EditorResult<ExceptionInfo>> {
|
||||
const json = JSON.stringify(this.unwrapOptionalsFromArrays(cloneDeep(config.configData)), null, 2);
|
||||
|
||||
return this.http.post<EditorResult<ExceptionInfo>>(
|
||||
`${this.config.serviceRoot}api/v1/${this.env}/configs/validate?singleConfig=true`, json);
|
||||
}
|
||||
|
||||
public validateRelease(deployment: Deployment<ConfigWrapper<ConfigData>>): Observable<EditorResult<ExceptionInfo>> {
|
||||
const validationFormat = this.marshalDeploymentFormat(deployment);
|
||||
const json = JSON.stringify(validationFormat, null, 2);
|
||||
|
||||
return this.http.post<EditorResult<ExceptionInfo>>(`${this.config.serviceRoot}api/v1/${this.env}/configs/validate`, json);
|
||||
}
|
||||
|
||||
public submitRelease(deployment: Deployment<ConfigWrapper<ConfigData>>): Observable<EditorResult<ExceptionInfo>> {
|
||||
const releaseFormat = this.marshalDeploymentFormat(deployment);
|
||||
const json = JSON.stringify(releaseFormat, null, 2);
|
||||
|
||||
return this.http.post<EditorResult<ExceptionInfo>>(`${this.config.serviceRoot}api/v1/${this.env}/configstore/release`, json);
|
||||
}
|
||||
|
||||
public submitConfigEdit(config: ConfigWrapper<ConfigData>): Observable<EditorResult<GitFiles<ConfigData>>> {
|
||||
const json = JSON.stringify(this.unwrapOptionalsFromArrays(cloneDeep(config.configData)), null, 2);
|
||||
|
||||
return this.http.put<EditorResult<GitFiles<ConfigData>>>(
|
||||
`${this.config.serviceRoot}api/v1/${this.env}/configstore/configs`, json);
|
||||
}
|
||||
|
||||
public submitNewConfig(config: ConfigWrapper<ConfigData>): Observable<EditorResult<GitFiles<ConfigData>>> {
|
||||
const json = JSON.stringify(this.unwrapOptionalsFromArrays(cloneDeep(config.configData)), null, 2);
|
||||
|
||||
return this.http.post<EditorResult<GitFiles<ConfigData>>>(
|
||||
`${this.config.serviceRoot}api/v1/${this.env}/configstore/configs`, json);
|
||||
}
|
||||
|
||||
public getFields(): Observable<Field[]> {
|
||||
return this.http.get<EditorResult<any>>(
|
||||
`${this.config.serviceRoot}api/v1/${this.env}/configs/fields`)
|
||||
.map(f => f.attributes.fields);
|
||||
}
|
||||
|
||||
public testDeploymentConfig(testDto: ConfigTestDto): Observable<EditorResult<ConfigTestResult>> {
|
||||
testDto.files[0].content = this.marshalDeploymentFormat(testDto.files[0].content);
|
||||
|
||||
return this.http.post<EditorResult<any>>(
|
||||
`${this.config.serviceRoot}api/v1/${this.env}/configs/test?singleConfig=false`, testDto)
|
||||
}
|
||||
|
||||
public testSingleConfig(testDto: ConfigTestDto): Observable<EditorResult<ConfigTestResult>> {
|
||||
testDto.files[0].content = this.unwrapOptionalsFromArrays(cloneDeep(testDto.files[0].content));
|
||||
|
||||
return this.http.post<EditorResult<any>>(
|
||||
`${this.config.serviceRoot}api/v1/${this.env}/configs/test?singleConfig=true`, testDto)
|
||||
}
|
||||
|
||||
public marshalDeploymentFormat(deployment: Deployment<ConfigWrapper<ConfigData>>): any {
|
||||
const d = cloneDeep(deployment);
|
||||
delete d.deploymentVersion;
|
||||
delete d.configs;
|
||||
|
||||
return Object.assign(d, {
|
||||
[this.uiMetadata.deployment.version]: deployment.deploymentVersion,
|
||||
[this.uiMetadata.deployment.config_array]:
|
||||
deployment.configs.map(config => this.unwrapOptionalsFromArrays(cloneDeep(config.configData))),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import 'rxjs/add/operator/map';
|
||||
import 'rxjs/add/operator/toPromise';
|
||||
import { ConfigData } from '../model/config-model';
|
||||
import { UiMetadataMap } from '../model/ui-metadata-map';
|
||||
import { EditorConfig } from './editor-config';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AppConfigService {
|
||||
|
||||
private config: EditorConfig;
|
||||
private uiMetadata: UiMetadataMap;
|
||||
|
||||
constructor(private http: HttpClient) { }
|
||||
|
||||
// This gets called on startup and APP_INITIALIZER will wait for the promise to resolve
|
||||
public loadConfig(): Promise<any> {
|
||||
return this.http.get('config.json')
|
||||
.toPromise()
|
||||
.then((r: ConfigData) => {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.info(`Loaded ${r.environment} config`, r);
|
||||
this.config = r;
|
||||
});
|
||||
}
|
||||
|
||||
public loadUiMetadata(): Promise<any> {
|
||||
return this.http.get('assets/uiSetupConfig.json')
|
||||
.toPromise()
|
||||
.then((r: UiMetadataMap) => {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.info('loaded UI setup', r);
|
||||
this.uiMetadata = r;
|
||||
})
|
||||
}
|
||||
|
||||
public getServiceList(): string[] {
|
||||
return Object.keys(this.uiMetadata);
|
||||
}
|
||||
|
||||
public getUiMetadata(serviceName: string): UiMetadataMap {
|
||||
return this.uiMetadata[serviceName];
|
||||
}
|
||||
|
||||
public getConfig(): EditorConfig {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
public get environment(): string {
|
||||
return this.config.environment;
|
||||
}
|
||||
|
||||
public get serviceRoot(): string {
|
||||
return this.config.serviceRoot;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
// import { HttpModule } from '@angular/http';
|
||||
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
import { AppConfigService } from './app-config.service';
|
||||
|
||||
export function configFactory(config: AppConfigService) {
|
||||
return config.getConfig();
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [HttpClientModule],
|
||||
providers: [AppConfigService],
|
||||
})
|
||||
export class ConfigModule {
|
||||
constructor() {}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export class EditorConfig {
|
||||
environment: string;
|
||||
serviceRoot: string;
|
||||
}
|
||||
3
config-editor/config-editor-ui/src/app/config/index.ts
Normal file
3
config-editor/config-editor-ui/src/app/config/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { EditorConfig } from './editor-config';
|
||||
export { AppConfigService } from './app-config.service';
|
||||
export { ConfigModule } from './config.module';
|
||||
@@ -0,0 +1,6 @@
|
||||
<div class="container">
|
||||
<re-nav-bar></re-nav-bar>
|
||||
<div class="router-holder">
|
||||
<router-outlet></router-outlet>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: stretch;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.router-holder {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 're-home',
|
||||
styleUrls: ['./home.component.scss'],
|
||||
templateUrl: './home.component.html',
|
||||
})
|
||||
export class HomeComponent {
|
||||
|
||||
constructor() {}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { HomeComponent } from './home/home.component';
|
||||
export { PageNotFoundComponent } from './page-not-found/page-not-found.component';
|
||||
@@ -0,0 +1,7 @@
|
||||
div {
|
||||
font-family: Roboto, "Helvetica Neue", sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
selector: 're-page-not-found',
|
||||
styleUrls: ['./page-not-found.component.scss'],
|
||||
template: '<div><mat-card><mat-card-content>Page not found!</mat-card-content></mat-card></div>',
|
||||
})
|
||||
export class PageNotFoundComponent { }
|
||||
16
config-editor/config-editor-ui/src/app/core/core.module.ts
Normal file
16
config-editor/config-editor-ui/src/app/core/core.module.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { NgModule, Optional, SkipSelf } from '@angular/core';
|
||||
import { EditorService } from 'app/editor.service';
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
EditorService,
|
||||
],
|
||||
})
|
||||
export class CoreModule {
|
||||
constructor( @Optional() @SkipSelf() parentModule: CoreModule) {
|
||||
if (parentModule) {
|
||||
throw new Error(
|
||||
'CoreModule is already loaded. Import it in the AppModule only ');
|
||||
}
|
||||
}
|
||||
}
|
||||
1
config-editor/config-editor-ui/src/app/core/index.ts
Normal file
1
config-editor/config-editor-ui/src/app/core/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { CoreModule } from './core.module';
|
||||
@@ -0,0 +1,14 @@
|
||||
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class CredentialsInterceptor implements HttpInterceptor {
|
||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
const clone = req.clone({ withCredentials: true });
|
||||
|
||||
return next.handle(clone);
|
||||
}
|
||||
}
|
||||
91
config-editor/config-editor-ui/src/app/editor.service.ts
Normal file
91
config-editor/config-editor-ui/src/app/editor.service.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { ConfigTestDto, DeploymentWrapper } from './model/config-model';
|
||||
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AppConfigService } from '@app/config';
|
||||
import { ConfigLoaderService } from '@app/config-loader.service';
|
||||
import {
|
||||
ConfigData,
|
||||
ConfigWrapper,
|
||||
Deployment,
|
||||
EditorResult,
|
||||
ExceptionInfo,
|
||||
GitFiles,
|
||||
PullRequestInfo,
|
||||
RepositoryLinks,
|
||||
SchemaDto,
|
||||
SensorFieldTemplate,
|
||||
UserName
|
||||
} from '@app/model';
|
||||
import { Field, SensorFields } from '@app/model/sensor-fields';
|
||||
import { StripSuffixPipe } from '@app/pipes';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export interface IConfigLoaderService {
|
||||
getConfigs(): Observable<ConfigWrapper<ConfigData>[]>;
|
||||
getConfigsFromFiles(files: any);
|
||||
getSchema(): Observable<SchemaDto>;
|
||||
getPullRequestStatus(): Observable<PullRequestInfo>;
|
||||
getRelease(): Observable<DeploymentWrapper>;
|
||||
getRepositoryLinks(): Observable<RepositoryLinks>;
|
||||
validateConfig(config: ConfigWrapper<ConfigData>): Observable<EditorResult<ExceptionInfo>>;
|
||||
validateRelease(deployment: Deployment<ConfigWrapper<ConfigData>>): Observable<EditorResult<ExceptionInfo>>;
|
||||
submitNewConfig(config: ConfigWrapper<ConfigData>): Observable<EditorResult<GitFiles<ConfigData>>>;
|
||||
submitConfigEdit(config: ConfigWrapper<ConfigData>): Observable<EditorResult<GitFiles<ConfigData>>>;
|
||||
submitRelease(deployment: Deployment<ConfigWrapper<ConfigData>>): Observable<EditorResult<ExceptionInfo>>;
|
||||
getFields(): Observable<Field[]>
|
||||
testDeploymentConfig(config: any): Observable<EditorResult<any>>;
|
||||
testSingleConfig(config: ConfigTestDto): Observable<EditorResult<any>>;
|
||||
};
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class EditorService {
|
||||
|
||||
loaderServices: Map<string, ConfigLoaderService> = new Map();
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private config: AppConfigService) {}
|
||||
|
||||
public getUser(): Observable<string> {
|
||||
return this.http.get<EditorResult<UserName>>(`${this.config.serviceRoot}user`)
|
||||
.map(result => new StripSuffixPipe().transform(result.attributes.user_name, '@UBERIT.NET'));
|
||||
}
|
||||
|
||||
public getServiceNames(): Observable<string[]> {
|
||||
return this.http.get<EditorResult<any>>(`${this.config.serviceRoot}user`)
|
||||
.map(result => result.attributes.services)
|
||||
.map(arr => arr.map(serviceName => serviceName.name));
|
||||
}
|
||||
|
||||
public getSensorFields(): Observable<SensorFields[]> {
|
||||
return this.http.get<EditorResult<SensorFieldTemplate>>(
|
||||
`${this.config.serviceRoot}api/v1/sensorfields`)
|
||||
.map(result => result.attributes.sensor_template_fields.sort((a, b) => {
|
||||
if (a.sensor_name > b.sensor_name) {
|
||||
return 1;
|
||||
} else if (a.sensor_name < b.sensor_name) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
public createLoaders() {
|
||||
this.config.getServiceList().forEach(element => {
|
||||
this.loaderServices.set(element, new ConfigLoaderService(this.http, this.config, element));
|
||||
});
|
||||
}
|
||||
|
||||
public getLoader(serviceName: string): any {
|
||||
try {
|
||||
return this.loaderServices.get(serviceName);
|
||||
} catch {
|
||||
throw new DOMException('Invalid service name - can\'t do nothing');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, CanActivate } from '@angular/router';
|
||||
import * as fromStore from '@app/store';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { combineLatest, Observable } from 'rxjs';
|
||||
import { filter, map, take, tap } from 'rxjs/operators';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ConfigStoreGuard implements CanActivate {
|
||||
|
||||
constructor(private store: Store<fromStore.State>) { }
|
||||
|
||||
canActivate(route: ActivatedRouteSnapshot): Observable<boolean> | Promise<boolean> | boolean {
|
||||
const id = parseInt(route.params['id'], 10);
|
||||
const serviceName = route.parent.url[0].path;
|
||||
|
||||
return isNaN(id)
|
||||
? false
|
||||
: combineLatest(this.store.select(fromStore.getBootstrapped), this.store.select(fromStore.getConfigs)).pipe(
|
||||
|
||||
tap(([bootstrapped, configs]) => {
|
||||
if (bootstrapped !== serviceName) {
|
||||
this.store.dispatch(new fromStore.Bootstrap(serviceName));
|
||||
}
|
||||
}),
|
||||
filter(([bootstrapped, configs]) => bootstrapped === serviceName),
|
||||
map(([isBootstrapped, configs]) => id < configs.length),
|
||||
take(1));
|
||||
}
|
||||
}
|
||||
3
config-editor/config-editor-ui/src/app/guards/index.ts
Normal file
3
config-editor/config-editor-ui/src/app/guards/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { ConfigStoreGuard } from './config-store.guard';
|
||||
export { ViewResolver } from './view.resolver';
|
||||
export { RepoResolver } from './repo.resolver';
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Resolve } from '@angular/router';
|
||||
import * as fromStore from '@app/store';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { map, take, tap } from 'rxjs/operators';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class RepoResolver implements Resolve<boolean> {
|
||||
|
||||
constructor(private store: Store<fromStore.State>) { }
|
||||
|
||||
resolve(): Observable<boolean> {
|
||||
return this.store.select(fromStore.getRepositoryLinks).pipe(
|
||||
tap(repositoryLinks => {
|
||||
if (!repositoryLinks) {
|
||||
this.store.dispatch(new fromStore.LoadRepositories());
|
||||
}
|
||||
}),
|
||||
map(repositoryLinks => true),
|
||||
take(1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Resolve } from '@angular/router';
|
||||
import * as fromStore from '@app/store';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { of } from 'rxjs';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { filter, map, take, tap } from 'rxjs/operators';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ViewResolver implements Resolve<boolean> {
|
||||
constructor(private store: Store<fromStore.State>) { }
|
||||
|
||||
resolve(route: ActivatedRouteSnapshot): Observable<boolean> {
|
||||
let serviceName = '';
|
||||
if (route.parent.url[0] === undefined) {
|
||||
return of(false);
|
||||
}
|
||||
serviceName = route.parent.url[0].path;
|
||||
|
||||
return this.store.select(fromStore.getBootstrapped).pipe(
|
||||
tap(bootstrapped => {
|
||||
if (bootstrapped !== serviceName) {
|
||||
this.store.dispatch(new fromStore.Bootstrap(serviceName));
|
||||
}
|
||||
}),
|
||||
map(bootstrapped => bootstrapped === serviceName),
|
||||
filter(isBootstrapped => isBootstrapped),
|
||||
take(1));
|
||||
}
|
||||
}
|
||||
102
config-editor/config-editor-ui/src/app/model/config-model.ts
Normal file
102
config-editor/config-editor-ui/src/app/model/config-model.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { SensorFields } from '@app/model';
|
||||
import { SchemaDto } from './schema';
|
||||
|
||||
export interface EditorResult<T> {
|
||||
status_code: string;
|
||||
attributes: T;
|
||||
}
|
||||
|
||||
export interface ExceptionInfo {
|
||||
exception?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface GitFiles<T> extends ExceptionInfo {
|
||||
files: T[];
|
||||
}
|
||||
|
||||
export interface GeneralRule {
|
||||
file_name: string;
|
||||
}
|
||||
|
||||
export interface ContentRuleFile<T> extends GeneralRule {
|
||||
content: T;
|
||||
}
|
||||
|
||||
export interface UserName extends ExceptionInfo {
|
||||
user_name: string;
|
||||
}
|
||||
|
||||
export interface RepositoryLinksWrapper extends ExceptionInfo {
|
||||
rules_repositories: RepositoryLinks;
|
||||
}
|
||||
|
||||
export interface RepositoryLinks {
|
||||
rule_store_url: string;
|
||||
rules_release_url: string;
|
||||
rulesetName: string;
|
||||
}
|
||||
|
||||
export interface SchemaInfo extends ExceptionInfo {
|
||||
rules_schema: any;
|
||||
}
|
||||
|
||||
export interface PullRequestInfo extends ExceptionInfo {
|
||||
pull_request_pending: boolean;
|
||||
pull_request_url: string;
|
||||
}
|
||||
|
||||
export interface ConfigWrapper<T> {
|
||||
versionFlag?: number;
|
||||
isDeployed?: boolean;
|
||||
isNew: boolean;
|
||||
configData: T;
|
||||
savedInBackend: boolean;
|
||||
name: string;
|
||||
author: string;
|
||||
version: number;
|
||||
description: string;
|
||||
tags?: string[];
|
||||
fileHistory?: FileHistory[];
|
||||
}
|
||||
|
||||
export interface FileHistory {
|
||||
author: string;
|
||||
date: string;
|
||||
removed: number;
|
||||
added: number;
|
||||
}
|
||||
|
||||
export interface ConfigTestDto {
|
||||
files: Deployment<ConfigData>,
|
||||
event: string,
|
||||
}
|
||||
|
||||
export type ConfigData = any;
|
||||
|
||||
export interface Deployment<T> {
|
||||
configs: T[];
|
||||
deploymentVersion: number;
|
||||
}
|
||||
|
||||
export interface BootstrapData {
|
||||
configs: ConfigWrapper<ConfigData>[],
|
||||
configSchema: SchemaDto,
|
||||
currentUser: string,
|
||||
pullRequestPending: PullRequestInfo,
|
||||
storedDeployment: Deployment<ConfigWrapper<ConfigData>>,
|
||||
sensorFields: SensorFields[],
|
||||
deploymentHistory?: FileHistory[],
|
||||
};
|
||||
|
||||
export interface ConfigTestResult {
|
||||
exception: string;
|
||||
message: string;
|
||||
test_result_output: string;
|
||||
test_result_complete: boolean;
|
||||
}
|
||||
|
||||
export interface DeploymentWrapper {
|
||||
storedDeployment: Deployment<ConfigWrapper<ConfigData>>;
|
||||
deploymentHistory: FileHistory[];
|
||||
}
|
||||
7
config-editor/config-editor-ui/src/app/model/index.ts
Normal file
7
config-editor/config-editor-ui/src/app/model/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export { SchemaDto } from './schema';
|
||||
export { GitFiles, UserName, SchemaInfo, EditorResult, ContentRuleFile,
|
||||
PullRequestInfo, ConfigWrapper, ConfigData, Deployment,
|
||||
BootstrapData, ExceptionInfo, RepositoryLinks, RepositoryLinksWrapper, FileHistory,
|
||||
} from './config-model';
|
||||
export { SubmitStatus } from './submit-status';
|
||||
export { SensorFieldTemplate, SensorFields } from './sensor-fields';
|
||||
11
config-editor/config-editor-ui/src/app/model/schema.ts
Normal file
11
config-editor/config-editor-ui/src/app/model/schema.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export class SchemaDto {
|
||||
schema: Schema;
|
||||
}
|
||||
|
||||
export interface Schema {
|
||||
description: string;
|
||||
properties: any;
|
||||
required: string[];
|
||||
title: string;
|
||||
type: string;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export interface SensorFieldTemplate {
|
||||
sensor_template_fields: SensorFields[];
|
||||
}
|
||||
|
||||
export interface SensorFields {
|
||||
fields: Field[];
|
||||
sensor_name: string;
|
||||
}
|
||||
|
||||
export interface Field {
|
||||
name: string;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export interface ISubmitStatus {
|
||||
submitInFlight: boolean;
|
||||
submitSuccess: boolean;
|
||||
statusCode: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export class SubmitStatus implements ISubmitStatus {
|
||||
public submitInFlight;
|
||||
public submitSuccess;
|
||||
public statusCode;
|
||||
public message;
|
||||
|
||||
constructor(
|
||||
submitInFlight: boolean = false,
|
||||
submitSuccess: boolean = false,
|
||||
message: string = undefined,
|
||||
statusCode: string = undefined) {
|
||||
this.submitInFlight = submitInFlight;
|
||||
this.submitSuccess = submitSuccess;
|
||||
this.statusCode = statusCode;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export interface UiMetadataMap {
|
||||
name: string,
|
||||
version: string,
|
||||
author: string,
|
||||
description: string,
|
||||
labelsFunc: string,
|
||||
testing: TestConfig,
|
||||
enableSensorFields: boolean,
|
||||
perConfigSchemaPath: string,
|
||||
deployment: DeploymentConfig,
|
||||
}
|
||||
|
||||
export interface TestConfig {
|
||||
perConfigTestEnabled: boolean,
|
||||
deploymentTestEnabled: boolean,
|
||||
helpMessage: string
|
||||
}
|
||||
|
||||
export interface DeploymentConfig {
|
||||
version: string,
|
||||
config_array: string,
|
||||
extras?: string[],
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { FieldArrayType } from '@ngx-formly/core';
|
||||
|
||||
@Component({
|
||||
// tslint:disable-next-line:component-selector
|
||||
selector: 'formly-array-type',
|
||||
template: `
|
||||
<legend *ngIf="to.label">{{ to.label }}</legend>
|
||||
<p class="description" *ngIf="to.description">{{ to.description }}</p>
|
||||
<div class="alert alert-danger" role="alert" *ngIf="showError && formControl.errors">
|
||||
<formly-validation-message [field]="field"></formly-validation-message>
|
||||
</div>
|
||||
<div *ngFor="let f of field.fieldGroup; let i = index;" class="row">
|
||||
<formly-field class="array-item" [field]="f"></formly-field>
|
||||
<div class="column">
|
||||
<a (click)="moveUp(i)">
|
||||
<mat-icon class="move-arrow" [class.greyed-out]="i <= 0">arrow_drop_up</mat-icon>
|
||||
</a>
|
||||
<a (click)="moveDown(i)">
|
||||
<mat-icon class="move-arrow" [class.greyed-out]="i >= field.fieldGroup.length -1">arrow_drop_down</mat-icon>
|
||||
</a>
|
||||
</div>
|
||||
<svg *ngIf="showRemoveButton"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="18" width="18" viewBox="0 0 24 24"
|
||||
class="close-button"
|
||||
(click)="remove(i)">
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="add-button" *ngIf="to.maxItems ? field.fieldGroup.length < to.maxItems : true">
|
||||
<button mat-raised-button color="primary" (click)="add()">Add to {{to.label}}</button>
|
||||
</div>
|
||||
<div>
|
||||
`,
|
||||
styles: [`
|
||||
legend {
|
||||
font-weight: 400;
|
||||
margin: 5px;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.description {
|
||||
padding: 0 10px 10px 15px;
|
||||
font-size: 0.9em;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.rhs {
|
||||
margin: 5px auto 5px 0;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
cursor: pointer;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
fill: orange;
|
||||
z-index: 500;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
margin: 5px 0 5px 0;
|
||||
}
|
||||
|
||||
.column {
|
||||
display: block;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.move-arrow {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.greyed-out {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
font-size: 18px;
|
||||
color: #707070;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.blank-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
opacity: 0;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.close-button:hover { background: rgba(255,255,255,0.2); }
|
||||
.array-item:hover > .close-button { visibility: visible }
|
||||
.array-item {
|
||||
flex: 9;
|
||||
position: relative;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
`],
|
||||
})
|
||||
export class ArrayTypeComponent extends FieldArrayType {
|
||||
public showRemoveButton = true;
|
||||
|
||||
moveUp (index: number) {
|
||||
if (index > 0) {
|
||||
this.reorder(index, index - 1);
|
||||
}
|
||||
}
|
||||
|
||||
moveDown (index: number) {
|
||||
if (index < this.field.fieldGroup.length - 1) {
|
||||
this.reorder(index, index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
reorder (oldIndex, newIndex) {
|
||||
const temp = this.model[oldIndex];
|
||||
this.remove(oldIndex);
|
||||
this.add(newIndex, temp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { FieldWrapper } from '@ngx-formly/core';
|
||||
|
||||
@Component({
|
||||
// tslint:disable-next-line:component-selector
|
||||
selector: 'formly-expansion-panel-wrapper',
|
||||
template: `
|
||||
<mat-expansion-panel [expanded]="true">
|
||||
<mat-expansion-panel-header>
|
||||
<mat-panel-title>
|
||||
{{ to.label }}
|
||||
</mat-panel-title>
|
||||
<mat-panel-description>
|
||||
{{ to.description }}
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
<ng-template matExpansionPanelContent #fieldComponent></ng-template>
|
||||
</mat-expansion-panel>
|
||||
`,
|
||||
})
|
||||
export class ExpansionPanelWrapperComponent extends FieldWrapper {
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import { FocusMonitor } from '@angular/cdk/a11y';
|
||||
import { AfterContentChecked,
|
||||
AfterViewInit, Component, ElementRef, OnDestroy, OnInit, Renderer2, TemplateRef, ViewChild } from '@angular/core';
|
||||
import { MatFormField } from '@angular/material/form-field';
|
||||
import { MatFormFieldControl } from '@angular/material/form-field';
|
||||
import { FieldWrapper, FormlyFieldConfig, ɵdefineHiddenProp as defineHiddenProp } from '@ngx-formly/core';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { FieldType } from '@ngx-formly/material';
|
||||
|
||||
interface MatFormlyFieldConfig extends FormlyFieldConfig {
|
||||
_matprefix: TemplateRef<any>;
|
||||
_matsuffix: TemplateRef<any>;
|
||||
__formField__: FormFieldWrapperComponent;
|
||||
_componentFactory: any;
|
||||
}
|
||||
|
||||
@Component({
|
||||
// tslint:disable-next-line:component-selector
|
||||
selector: 'formly-wrapper-mat-form-field',
|
||||
template: `
|
||||
<!-- fix https://github.com/angular/material2/pull/7083 by setting width to 100% -->
|
||||
<mat-form-field
|
||||
[hideRequiredMarker]="true"
|
||||
[floatLabel]="to.floatLabel"
|
||||
[appearance]="to.appearance"
|
||||
[color]="to.color"
|
||||
[style.width]="'100%'">
|
||||
<ng-container #fieldComponent></ng-container>
|
||||
<mat-label *ngIf="to.label && to.hideLabel !== true">
|
||||
{{ to.label }}
|
||||
<span *ngIf="to.required && to.hideRequiredMarker !== true" class="mat-form-field-required-marker">*</span>
|
||||
</mat-label>
|
||||
<ng-container matPrefix>
|
||||
<ng-container *ngTemplateOutlet="to.prefix ? to.prefix : formlyField._matprefix"></ng-container>
|
||||
</ng-container>
|
||||
<ng-container matSuffix>
|
||||
<ng-container *ngTemplateOutlet="to.suffix ? to.suffix : formlyField._matsuffix"></ng-container>
|
||||
</ng-container>
|
||||
<!-- fix https://github.com/angular/material2/issues/7737 by setting id to null -->
|
||||
<mat-error [id]="null">
|
||||
<formly-validation-message [field]="field"></formly-validation-message>
|
||||
</mat-error>
|
||||
<!-- fix https://github.com/angular/material2/issues/7737 by setting id to null -->
|
||||
<mat-hint *ngIf="to.description" [id]="null" align="end">{{ to.description }}</mat-hint>
|
||||
</mat-form-field>
|
||||
`,
|
||||
providers: [{ provide: MatFormFieldControl, useExisting: FormFieldWrapperComponent }],
|
||||
})
|
||||
export class FormFieldWrapperComponent extends FieldWrapper<MatFormlyFieldConfig>
|
||||
implements OnInit, OnDestroy, MatFormFieldControl<any>, AfterViewInit, AfterContentChecked {
|
||||
// TODO: remove `any`, once dropping angular `V7` support.
|
||||
@ViewChild(MatFormField, <any> { static: true }) formField!: MatFormField;
|
||||
|
||||
stateChanges = new Subject<void>();
|
||||
_errorState = false;
|
||||
private initialGapCalculated = false;
|
||||
|
||||
constructor(
|
||||
private renderer: Renderer2,
|
||||
private elementRef: ElementRef,
|
||||
private focusMonitor: FocusMonitor
|
||||
) {
|
||||
super();
|
||||
|
||||
focusMonitor.monitor(elementRef, true).subscribe(origin => {
|
||||
this.field.focus = !!origin;
|
||||
this.stateChanges.next();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.formField._control = this;
|
||||
defineHiddenProp(this.field, '__formField__', this.formField);
|
||||
|
||||
const fieldComponent = this.formlyField['_componentFactory'];
|
||||
if (fieldComponent && !(fieldComponent.componentRef.instance instanceof FieldType)) {
|
||||
console.warn(
|
||||
`Component '${fieldComponent.component.prototype.constructor.name}' must extend 'FieldType' from '@ngx-formly/material'.`);
|
||||
}
|
||||
|
||||
// fix for https://github.com/angular/material2/issues/11437
|
||||
if (this.formlyField.hide && this.formlyField.templateOptions!.appearance === 'outline') {
|
||||
this.initialGapCalculated = true;
|
||||
}
|
||||
}
|
||||
|
||||
ngAfterContentChecked() {
|
||||
if (!this.initialGapCalculated || this.formlyField.hide) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.formField.updateOutlineGap();
|
||||
this.initialGapCalculated = true;
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
// temporary fix for https://github.com/angular/material2/issues/7891
|
||||
if (this.formField.underlineRef && this.to.hideFieldUnderline === true) {
|
||||
this.renderer.removeClass(this.formField.underlineRef.nativeElement, 'mat-form-field-underline');
|
||||
this.renderer.removeClass(this.formField.underlineRef.nativeElement.firstChild, 'mat-form-field-ripple');
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
delete this.formlyField.__formField__;
|
||||
this.stateChanges.complete();
|
||||
this.focusMonitor.stopMonitoring(this.elementRef);
|
||||
}
|
||||
|
||||
setDescribedByIds(ids: string[]): void { }
|
||||
onContainerClick(event: MouseEvent): void {
|
||||
this.formlyField.focus = true;
|
||||
this.stateChanges.next();
|
||||
}
|
||||
|
||||
get errorState() {
|
||||
const showError = this.options!.showError!(this);
|
||||
if (showError !== this._errorState) {
|
||||
this._errorState = showError;
|
||||
this.stateChanges.next();
|
||||
}
|
||||
|
||||
return showError;
|
||||
}
|
||||
get controlType() { return this.to.type; }
|
||||
get focused() { return !!this.formlyField.focus && !this.disabled; }
|
||||
get disabled() { return !!this.to.disabled; }
|
||||
get required() { return !!this.to.required; }
|
||||
get placeholder() { return this.to.placeholder || ''; }
|
||||
get shouldPlaceholderFloat() { return this.shouldLabelFloat; }
|
||||
get value() { return this.formControl.value; }
|
||||
get ngControl() { return this.formControl as any; }
|
||||
get empty() { return !this.formControl.value; }
|
||||
get shouldLabelFloat() { return this.focused || !this.empty; }
|
||||
|
||||
get formlyField() { return this.field as MatFormlyFieldConfig; }
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { MatInput } from '@angular/material/input';
|
||||
import { FieldType } from '@ngx-formly/material/form-field';
|
||||
|
||||
@Component({
|
||||
selector: 'formly-field-mat-input',
|
||||
template: `
|
||||
<input *ngIf="type !== 'number'; else numberTmp"
|
||||
matInput
|
||||
[name]="to.title"
|
||||
[id]="id"
|
||||
[readonly]="to.readonly"
|
||||
[type]="type || 'text'"
|
||||
[errorStateMatcher]="errorStateMatcher"
|
||||
[formControl]="formControl"
|
||||
[formlyAttributes]="field"
|
||||
[tabindex]="to.tabindex || 0"
|
||||
[placeholder]="to.placeholder">
|
||||
<ng-template #numberTmp>
|
||||
<input matInput
|
||||
[id]="id"
|
||||
type="number"
|
||||
[readonly]="to.readonly"
|
||||
[errorStateMatcher]="errorStateMatcher"
|
||||
[formControl]="formControl"
|
||||
[formlyAttributes]="field"
|
||||
[tabindex]="to.tabindex || 0"
|
||||
[placeholder]="to.placeholder">
|
||||
</ng-template>
|
||||
`,
|
||||
})
|
||||
export class InputTypeComponent extends FieldType implements OnInit {
|
||||
@ViewChild(MatInput, <any> { static: true }) formFieldControl!: MatInput;
|
||||
|
||||
get type() {
|
||||
return this.to.type || 'text';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { FieldType } from '@ngx-formly/core';
|
||||
|
||||
@Component({
|
||||
selector: 'formly-null-type',
|
||||
template: '',
|
||||
})
|
||||
export class NullTypeComponent extends FieldType {}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { FieldType } from '@ngx-formly/core';
|
||||
|
||||
@Component({
|
||||
// tslint:disable-next-line:component-selector
|
||||
selector: 'formly-object-type',
|
||||
template: `
|
||||
<div class="alert alert-danger" role="alert" *ngIf="showError && formControl.errors">
|
||||
<formly-validation-message [field]="field"></formly-validation-message>
|
||||
</div>
|
||||
<formly-field *ngFor="let f of field.fieldGroup" [field]="f"></formly-field>
|
||||
`,
|
||||
styles: [`
|
||||
mat-card:last-child {
|
||||
display: none;
|
||||
}
|
||||
`],
|
||||
})
|
||||
export class ObjectTypeComponent extends FieldType {
|
||||
defaultOptions = {
|
||||
defaultValue: {},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { FieldWrapper } from '@ngx-formly/core';
|
||||
|
||||
@Component({
|
||||
// tslint:disable-next-line:component-selector
|
||||
selector: 'formly-wrapper-panel',
|
||||
template: `
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>
|
||||
{{ to.label }}
|
||||
</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<p class="description" *ngIf="to.description">{{ to.description }}</p>
|
||||
<ng-container #fieldComponent></ng-container>
|
||||
</mat-card-content>
|
||||
<mat-card>
|
||||
`,
|
||||
styles: [`
|
||||
.description {
|
||||
padding: 0 10px 10px 15px;
|
||||
font-size: 0.9em;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.mat-card:nth-child(3) {
|
||||
display: none;
|
||||
}
|
||||
`],
|
||||
})
|
||||
export class PanelWrapperComponent extends FieldWrapper {
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Component, ViewChild, ViewContainerRef } from '@angular/core';
|
||||
import { FieldWrapper } from '@ngx-formly/core';
|
||||
|
||||
@Component({
|
||||
// tslint:disable-next-line:component-selector
|
||||
selector: 'formly-wrapper-tabs',
|
||||
template:
|
||||
`<mat-tab-group>
|
||||
<mat-tab *ngFor="let tab of field.fieldGroup" [label]="to.label">
|
||||
<ng-template #fieldComponent></ng-template>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
`,
|
||||
})
|
||||
export class TabsWrapperComponent extends FieldWrapper {
|
||||
@ViewChild('fieldComponent', { read: ViewContainerRef, static: true }) fieldComponent: ViewContainerRef;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { FieldType } from '@ngx-formly/core';
|
||||
|
||||
@Component({
|
||||
// tslint:disable-next-line:component-selector
|
||||
selector: 'formly-tabset-type',
|
||||
template:
|
||||
`<mat-tab-group animationDuration="0ms">
|
||||
<mat-tab *ngFor="let tab of field.fieldGroup" [label]="tab?.templateOptions?.label">
|
||||
<formly-field [field]="tab"></formly-field>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
`,
|
||||
})
|
||||
export class TabsetTypeComponent extends FieldType {
|
||||
defaultOptions = {
|
||||
defaultValue: {},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import { AfterViewInit, Component, ElementRef, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { MatAutocompleteSelectedEvent, MatAutocompleteTrigger } from '@angular/material';
|
||||
import { MatInput } from '@angular/material/input';
|
||||
import { SensorFields } from '@app/model';
|
||||
import { FieldType } from '@ngx-formly/material/form-field';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { debounceTime, takeUntil, tap } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
// tslint:disable-next-line:component-selector
|
||||
selector: 'formly-field-mat-textarea',
|
||||
template: `
|
||||
<div class="overlay-holder">
|
||||
<mat-form-field #formfield>
|
||||
<mat-label>{{ to.label }}</mat-label>
|
||||
<textarea highlight matInput #textbox #autocompleteInput
|
||||
[matAutocomplete]="auto"
|
||||
[class.hide-text]="true"
|
||||
[id]="id"
|
||||
[name]="to.title"
|
||||
[readonly]="to.readonly"
|
||||
[formControl]="formControl"
|
||||
[errorStateMatcher]="errorStateMatcher"
|
||||
[formlyAttributes]="field"
|
||||
[placeholder]="to.placeholder"
|
||||
[tabindex]="to.tabindex || 0"
|
||||
[readonly]="to.readonly"
|
||||
(ngModelChange)="resizeTextArea($event)"
|
||||
>
|
||||
</textarea>
|
||||
<mat-autocomplete #auto="matAutocomplete" [autoActiveFirstOption]='true' (optionSelected)="autoCompleteSelected($event)">
|
||||
<mat-optgroup *ngFor="let group of filteredList" [label]="group.sensor_name">
|
||||
<mat-option *ngFor="let field of group?.fields" [value]="field?.name">
|
||||
{{field.name}}
|
||||
</mat-option>
|
||||
</mat-optgroup>
|
||||
</mat-autocomplete>
|
||||
<mat-hint *ngIf="to?.description && (!to?.errorMessage)"
|
||||
align="end" [innerHTML]="to?.description"></mat-hint>
|
||||
</mat-form-field>
|
||||
<div class="highlighted-overlay" [class.show-overlay]="true" [innerHtml]="modelValue | highlightVariables"></div>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
textarea {
|
||||
resize: none;
|
||||
line-height: normal;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hide-text {
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.show-overlay {
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.highlighted-overlay {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.overlay-holder {
|
||||
position: relative;
|
||||
line-height: normal;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::ng-deep .overlay-holder .mat-input-element {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
}
|
||||
`],
|
||||
})
|
||||
export class TextAreaTypeComponent extends FieldType implements OnDestroy, AfterViewInit {
|
||||
@ViewChild(MatInput, {static: false}) formFieldControl!: MatInput;
|
||||
|
||||
@ViewChild('textbox', {static: true}) textbox: ElementRef;
|
||||
@ViewChild('autocompleteInput', { read: MatAutocompleteTrigger, static: false }) autocomplete: MatAutocompleteTrigger;
|
||||
|
||||
@ViewChild('formfield', {static: true}) formfield: FormControl;
|
||||
|
||||
public displayOverlay = true;
|
||||
public modelValue;
|
||||
sensorFields$: Observable<SensorFields[]>;
|
||||
autoCompleteList: SensorFields[];
|
||||
filteredList: SensorFields[] = [];
|
||||
sensorDataSource$: any;
|
||||
_value;
|
||||
private _prevValue;
|
||||
|
||||
|
||||
private ngUnsubscribe: Subject<any> = new Subject();
|
||||
private readonly variableRegex = new RegExp(/\${([a-zA-Z_.:]*)(?![^ ]*})/);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.ngUnsubscribe.next();
|
||||
this.ngUnsubscribe.complete();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
// populate the model value with the current form value
|
||||
this.modelValue = this.value;
|
||||
this.resizeTextArea(this.field.model[this.field.key]);
|
||||
this.formControl.valueChanges.pipe(tap(v => this.modelValue = v), debounceTime(400), takeUntil(this.ngUnsubscribe)).subscribe(val => {
|
||||
if (val === null || val === undefined || !this.options.formState.sensorFields) {
|
||||
return;
|
||||
}
|
||||
this._prevValue = this._value;
|
||||
this._value = val;
|
||||
this.autoCompleteList = this.options.formState.sensorFields;
|
||||
this.filteredList = cloneDeep(this.options.formState.sensorFields);
|
||||
const matches = this.variableRegex.exec(val);
|
||||
if (matches != null && matches.length > 0) {
|
||||
for (let i = 0; i < this.options.formState.sensorFields.length; ++i) {
|
||||
this.filteredList[i].fields = this.options.formState.sensorFields[i].fields
|
||||
.filter(n => n.name.includes(matches[matches.length - 1]));
|
||||
}
|
||||
this.autocomplete.autocompleteDisabled = false;
|
||||
this.autocomplete.openPanel();
|
||||
} else {
|
||||
this.autocomplete.closePanel();
|
||||
this.autocomplete.autocompleteDisabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
resizeTextArea(event) {
|
||||
const textarea = this.textbox.nativeElement as HTMLTextAreaElement;
|
||||
// scrollheight needs some persuading to tell us what the new height should be
|
||||
textarea.style.height = '20px';
|
||||
if (textarea.scrollHeight !== 0) {
|
||||
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||
}
|
||||
}
|
||||
|
||||
public autoCompleteSelected($event: MatAutocompleteSelectedEvent) {
|
||||
const replacementStr = this._prevValue.replace(this.variableRegex, '${' + $event.option.value + '}');
|
||||
this.field.formControl.setValue(replacementStr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
import { TitleCasePipe } from '@angular/common';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AbstractControl } from '@angular/forms';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { FormlyFieldConfig } from '@ngx-formly/core';
|
||||
import { ɵreverseDeepMerge as reverseDeepMerge } from '@ngx-formly/core';
|
||||
import * as fromStore from 'app/store';
|
||||
import { JSONSchema7, JSONSchema7TypeName } from 'json-schema';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
export interface FormlyJsonschemaOptions {
|
||||
/**
|
||||
* allows to intercept the mapping, taking the already mapped
|
||||
* formly field and the original JSONSchema source from which it
|
||||
* was mapped.
|
||||
*/
|
||||
map?: (mappedField: FormlyFieldConfig, mapSource: JSONSchema7) => FormlyFieldConfig;
|
||||
}
|
||||
|
||||
function isEmpty(v) {
|
||||
return v === '' || v === undefined || v === null;
|
||||
}
|
||||
|
||||
interface IOptions extends FormlyJsonschemaOptions {
|
||||
schema: JSONSchema7;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class FormlyJsonschema {
|
||||
private dynamicFieldsMap: Map<string, string>;
|
||||
|
||||
titleCasePipe: TitleCasePipe = new TitleCasePipe();
|
||||
|
||||
constructor(private store: Store<fromStore.State>) {}
|
||||
|
||||
toFieldConfig(schema: JSONSchema7, options?: FormlyJsonschemaOptions): FormlyFieldConfig {
|
||||
this.dynamicFieldsMap = new Map<string, string>();
|
||||
const fieldConfig = this._toFieldConfig(schema, { schema, ...(options || {}) }, []);
|
||||
this.store.dispatch(new fromStore.UpdateDynamicFieldsMap(this.dynamicFieldsMap));
|
||||
|
||||
return fieldConfig;
|
||||
}
|
||||
|
||||
private _toFieldConfig(schema: JSONSchema7, options: IOptions, propKey?: string[]): FormlyFieldConfig {
|
||||
if (schema.$ref) {
|
||||
schema = this.resolveDefinition(schema, options);
|
||||
}
|
||||
|
||||
if (schema.allOf) {
|
||||
schema = this.resolveAllOf(schema, options);
|
||||
}
|
||||
|
||||
let field: FormlyFieldConfig = {
|
||||
type: this.guessType(schema),
|
||||
defaultValue: schema.default,
|
||||
templateOptions: {
|
||||
label: schema.title ? this.titleCasePipe.transform(<string> schema.title.replace(/_/g, ' ')) : '',
|
||||
readonly: schema.readOnly,
|
||||
description: schema.description,
|
||||
},
|
||||
};
|
||||
|
||||
switch (field.type) {
|
||||
case 'null': {
|
||||
this.addValidator(field, 'null', c => c.value === null);
|
||||
break;
|
||||
}
|
||||
case 'boolean': {
|
||||
field.templateOptions.label = this.titleCasePipe.transform(propKey[propKey.length - 1].replace(/_/g, ' '));
|
||||
field.templateOptions.description = schema.description;
|
||||
break;
|
||||
}
|
||||
case 'number':
|
||||
case 'integer': {
|
||||
field.templateOptions.label = this.titleCasePipe.transform(propKey[propKey.length - 1].replace(/_/g, ' '));
|
||||
field.parsers = [v => isEmpty(v) ? null : Number(v)];
|
||||
if (schema.hasOwnProperty('minimum')) {
|
||||
field.templateOptions.min = schema.minimum;
|
||||
}
|
||||
|
||||
if (schema.hasOwnProperty('maximum')) {
|
||||
field.templateOptions.max = schema.maximum;
|
||||
}
|
||||
|
||||
if (schema.hasOwnProperty('exclusiveMinimum')) {
|
||||
field.templateOptions.exclusiveMinimum = schema.exclusiveMinimum;
|
||||
this.addValidator(field, 'exclusiveMinimum', c => isEmpty(c.value) || (c.value > schema.exclusiveMinimum));
|
||||
}
|
||||
|
||||
if (schema.hasOwnProperty('exclusiveMaximum')) {
|
||||
field.templateOptions.exclusiveMaximum = schema.exclusiveMaximum;
|
||||
this.addValidator(field, 'exclusiveMaximum', c => isEmpty(c.value) || (c.value < schema.exclusiveMaximum));
|
||||
}
|
||||
|
||||
if (schema.hasOwnProperty('multipleOf')) {
|
||||
field.templateOptions.step = schema.multipleOf;
|
||||
this.addValidator(field, 'multipleOf', c => isEmpty(c.value) || (c.value % schema.multipleOf === 0));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'string': {
|
||||
['minLength', 'maxLength', 'pattern'].forEach(prop => {
|
||||
if (schema.hasOwnProperty(prop)) {
|
||||
field.templateOptions[prop] = schema[prop];
|
||||
}
|
||||
});
|
||||
field.templateOptions.label = propKey[propKey.length - 1] !== '-'
|
||||
? this.titleCasePipe.transform(propKey[propKey.length - 1].replace(/_/g, ' '))
|
||||
: '';
|
||||
break;
|
||||
}
|
||||
case 'object': {
|
||||
field.fieldGroup = [];
|
||||
|
||||
const [propDeps, schemaDeps] = this.resolveDependencies(schema);
|
||||
Object.keys(schema.properties || {}).forEach(key => {
|
||||
const newPropKey = cloneDeep(propKey);
|
||||
newPropKey.push(key);
|
||||
const f = this._toFieldConfig(<JSONSchema7> schema.properties[key], options, newPropKey);
|
||||
field.fieldGroup.push(f);
|
||||
f.key = key;
|
||||
if (Array.isArray(schema.required) && schema.required.indexOf(key) !== -1) {
|
||||
f.templateOptions.required = true;
|
||||
}
|
||||
if (!f.templateOptions.required && propDeps[key]) {
|
||||
f.expressionProperties = {
|
||||
'templateOptions.required': m => m && propDeps[key].some(k => !isEmpty(m[k])),
|
||||
};
|
||||
}
|
||||
|
||||
if (schemaDeps[key]) {
|
||||
field.fieldGroup.push({
|
||||
...this._toFieldConfig(schemaDeps[key], options),
|
||||
hideExpression: m => !m || isEmpty(m[key]),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case 'array': {
|
||||
field.fieldGroup = [];
|
||||
field.templateOptions.label = this.titleCasePipe.transform(propKey[propKey.length - 1].replace(/_/g, ' '));
|
||||
const newPropKey2 = cloneDeep(propKey);
|
||||
|
||||
if (schema.hasOwnProperty('minItems')) {
|
||||
field.templateOptions.minItems = schema.minItems;
|
||||
this.addValidator(field, 'minItems', c => isEmpty(c.value) || (c.value.length >= schema.minItems));
|
||||
}
|
||||
if (schema.hasOwnProperty('maxItems')) {
|
||||
field.templateOptions.maxItems = schema.maxItems;
|
||||
this.addValidator(field, 'maxItems', c => isEmpty(c.value) || (c.value.length <= schema.maxItems));
|
||||
}
|
||||
|
||||
Object.defineProperty(field, 'fieldArray', {
|
||||
get: () => {
|
||||
if (!Array.isArray(schema.items)) {
|
||||
// When items is a single schema, the additionalItems keyword is meaningless, and it should not be used.
|
||||
if (newPropKey2[newPropKey2.length - 1] !== '-') {
|
||||
newPropKey2.push('-');
|
||||
}
|
||||
|
||||
return this._toFieldConfig(<JSONSchema7> schema.items, options, newPropKey2);
|
||||
}
|
||||
|
||||
const itemSchema = schema.items[field.fieldGroup.length]
|
||||
? schema.items[field.fieldGroup.length]
|
||||
: schema.additionalItems;
|
||||
|
||||
return itemSchema
|
||||
? this._toFieldConfig(<JSONSchema7> itemSchema, options)
|
||||
: null;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.hasOwnProperty('x-schema-form')) {
|
||||
if (schema['x-schema-form'].hasOwnProperty('type')) {
|
||||
field.type = schema['x-schema-form'].type;
|
||||
}
|
||||
if (schema['x-schema-form'].hasOwnProperty('wrappers')) {
|
||||
field.wrappers = schema['x-schema-form'].wrappers;
|
||||
} else if (field.type === 'object') {
|
||||
field.wrappers = ['panel']
|
||||
}
|
||||
if (schema['x-schema-form'].hasOwnProperty('condition')) {
|
||||
if (schema['x-schema-form'].condition.hasOwnProperty('hideExpression')) {
|
||||
try {
|
||||
const dynFunc: Function =
|
||||
new Function('model', 'localFields', 'field', schema['x-schema-form'].condition.hideExpression);
|
||||
field.hideExpression = (model, formState, f) => dynFunc(formState.mainModel, model, f);
|
||||
} catch {
|
||||
console.warn('Something went wrong with applying condition evaluation to form');
|
||||
}
|
||||
this.dynamicFieldsMap.set(('/' + propKey.join('/')), schema['x-schema-form'].condition.hideExpression);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.enum) {
|
||||
field.type = 'enum';
|
||||
field.templateOptions.options = schema.enum.map(value => ({ value, label: value }));
|
||||
}
|
||||
|
||||
// map in possible formlyConfig options from the widget property
|
||||
if (schema['widget'] && schema['widget'].formlyConfig) {
|
||||
field = reverseDeepMerge(schema['widget'].formlyConfig, field);
|
||||
}
|
||||
|
||||
// if there is a map function passed in, use it to allow the user to
|
||||
// further customize how fields are being mapped
|
||||
return options.map ? options.map(field, schema) : field;
|
||||
}
|
||||
|
||||
private resolveAllOf({ allOf, ...baseSchema }: JSONSchema7, options: IOptions) {
|
||||
if (!allOf.length) {
|
||||
throw Error(`allOf array can not be empty ${allOf}.`);
|
||||
}
|
||||
|
||||
return allOf.reduce((base: JSONSchema7, schema: JSONSchema7) => {
|
||||
if (schema.$ref) {
|
||||
schema = this.resolveDefinition(schema, options);
|
||||
}
|
||||
|
||||
if (schema.allOf) {
|
||||
schema = this.resolveAllOf(schema, options);
|
||||
}
|
||||
if (base.required && schema.required) {
|
||||
base.required = [...base.required, ...schema.required];
|
||||
}
|
||||
|
||||
if (schema.uniqueItems) {
|
||||
base.uniqueItems = schema.uniqueItems;
|
||||
}
|
||||
|
||||
// resolve to min value
|
||||
['maxLength', 'maximum', 'exclusiveMaximum', 'maxItems', 'maxProperties']
|
||||
.forEach(prop => {
|
||||
if (!isEmpty(base[prop]) && !isEmpty(schema[prop])) {
|
||||
base[prop] = base[prop] < schema[prop] ? base[prop] : schema[prop];
|
||||
}
|
||||
});
|
||||
|
||||
// resolve to max value
|
||||
['minLength', 'minimum', 'exclusiveMinimum', 'minItems', 'minProperties']
|
||||
.forEach(prop => {
|
||||
if (!isEmpty(base[prop]) && !isEmpty(schema[prop])) {
|
||||
base[prop] = base[prop] > schema[prop] ? base[prop] : schema[prop];
|
||||
}
|
||||
});
|
||||
|
||||
return reverseDeepMerge(base, schema);
|
||||
}, baseSchema);
|
||||
}
|
||||
|
||||
private resolveDefinition(schema: JSONSchema7, options: IOptions): JSONSchema7 {
|
||||
const [uri, pointer] = schema.$ref.split('#/');
|
||||
if (uri) {
|
||||
throw Error(`Remote schemas for ${schema.$ref} not supported yet.`);
|
||||
}
|
||||
|
||||
const definition = !pointer ? null : pointer.split('/').reduce(
|
||||
(def, path) => def && def.hasOwnProperty(path) ? def[path] : null,
|
||||
options.schema
|
||||
);
|
||||
|
||||
if (!definition) {
|
||||
throw Error(`Cannot find a definition for ${schema.$ref}.`);
|
||||
}
|
||||
|
||||
if (definition.$ref) {
|
||||
return this.resolveDefinition(definition, options);
|
||||
}
|
||||
|
||||
return {
|
||||
...definition,
|
||||
...['title', 'description', 'default'].reduce((annotation, p) => {
|
||||
if (schema.hasOwnProperty(p)) {
|
||||
annotation[p] = schema[p];
|
||||
}
|
||||
|
||||
return annotation;
|
||||
}, {}),
|
||||
};
|
||||
}
|
||||
|
||||
private resolveDependencies(schema: JSONSchema7) {
|
||||
const deps = {};
|
||||
const schemaDeps = {};
|
||||
|
||||
Object.keys(schema.dependencies || {}).forEach(prop => {
|
||||
const dependency = schema.dependencies[prop] as JSONSchema7;
|
||||
if (Array.isArray(dependency)) {
|
||||
// Property dependencies
|
||||
dependency.forEach(dep => {
|
||||
if (!deps[dep]) {
|
||||
deps[dep] = [prop];
|
||||
} else {
|
||||
deps[dep].push(prop);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// schema dependencies
|
||||
schemaDeps[prop] = dependency;
|
||||
}
|
||||
});
|
||||
|
||||
return [deps, schemaDeps];
|
||||
}
|
||||
|
||||
private guessType(schema: JSONSchema7) {
|
||||
const type = schema.type as JSONSchema7TypeName;
|
||||
if (!type && schema.properties) {
|
||||
return 'object';
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
private addValidator(field: FormlyFieldConfig, name: string, validator: (control: AbstractControl) => boolean) {
|
||||
field.validators = field.validators || {};
|
||||
field.validators[name] = validator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { hasOwn } from './utility.functions';
|
||||
import {
|
||||
isArray,
|
||||
isDefined,
|
||||
isMap,
|
||||
isObject,
|
||||
isString
|
||||
} from './validator.functions';
|
||||
|
||||
|
||||
/**
|
||||
* 'JsonPointer' class
|
||||
*
|
||||
* Some utilities for using JSON Pointers with JSON objects
|
||||
* https://tools.ietf.org/html/rfc6901
|
||||
*
|
||||
* get, getCopy, getFirst, set, setCopy, insert, insertCopy, remove, has, dict,
|
||||
* forEachDeep, forEachDeepCopy, escape, unescape, parse, compile, toKey,
|
||||
* isJsonPointer, isSubPointer, toIndexedPointer, toGenericPointer,
|
||||
* toControlPointer, toSchemaPointer, toDataPointer, parseObjectPath
|
||||
*
|
||||
* Some functions based on manuelstofer's json-pointer utilities
|
||||
* https://github.com/manuelstofer/json-pointer
|
||||
*/
|
||||
export type Pointer = string | string[];
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class JsonPointer {
|
||||
|
||||
/**
|
||||
* 'get' function
|
||||
*
|
||||
* Uses a JSON Pointer to retrieve a value from an object.
|
||||
*
|
||||
* // { object } object - Object to get value from
|
||||
* // { Pointer } pointer - JSON Pointer (string or array)
|
||||
* // { number = 0 } startSlice - Zero-based index of first Pointer key to use
|
||||
* // { number } endSlice - Zero-based index of last Pointer key to use
|
||||
* // { boolean = false } getBoolean - Return only true or false?
|
||||
* // { boolean = false } errors - Show error if not found?
|
||||
* // { object } - Located value (or true or false if getBoolean = true)
|
||||
*/
|
||||
static get(
|
||||
object, pointer, startSlice = 0, endSlice: number = null,
|
||||
getBoolean = false, errors = false
|
||||
) {
|
||||
if (object === null) { return getBoolean ? false : undefined; }
|
||||
let keyArray: any[] = this.parse(pointer, errors);
|
||||
if (typeof object === 'object' && keyArray !== null) {
|
||||
let subObject = object;
|
||||
if (startSlice >= keyArray.length || endSlice <= -keyArray.length) { return object; }
|
||||
if (startSlice <= -keyArray.length) { startSlice = 0; }
|
||||
if (!isDefined(endSlice) || endSlice >= keyArray.length) { endSlice = keyArray.length; }
|
||||
keyArray = keyArray.slice(startSlice, endSlice);
|
||||
for (let key of keyArray) {
|
||||
if (key === '-' && isArray(subObject) && subObject.length) {
|
||||
key = subObject.length - 1;
|
||||
}
|
||||
if (isMap(subObject) && subObject.has(key)) {
|
||||
subObject = subObject.get(key);
|
||||
} else if (typeof subObject === 'object' && subObject !== null &&
|
||||
hasOwn(subObject, key)
|
||||
) {
|
||||
subObject = subObject[key];
|
||||
} else {
|
||||
if (errors) {
|
||||
console.error(`get error: "${key}" key not found in object.`);
|
||||
console.error(pointer);
|
||||
console.error(object);
|
||||
}
|
||||
return getBoolean ? false : undefined;
|
||||
}
|
||||
}
|
||||
return getBoolean ? true : subObject;
|
||||
}
|
||||
if (errors && keyArray === null) {
|
||||
console.error(`get error: Invalid JSON Pointer: ${pointer}`);
|
||||
}
|
||||
if (errors && typeof object !== 'object') {
|
||||
console.error('get error: Invalid object:');
|
||||
console.error(object);
|
||||
}
|
||||
return getBoolean ? false : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 'remove' function
|
||||
*
|
||||
* Uses a JSON Pointer to remove a key and its attribute from an object
|
||||
*
|
||||
* // { object } object - object to delete attribute from
|
||||
* // { Pointer } pointer - JSON Pointer (string or array)
|
||||
* // { object }
|
||||
*/
|
||||
static remove(object, pointer) {
|
||||
const keyArray = this.parse(pointer);
|
||||
if (keyArray !== null && keyArray.length) {
|
||||
let lastKey = keyArray.pop();
|
||||
const parentObject = this.get(object, keyArray);
|
||||
if (isArray(parentObject)) {
|
||||
if (lastKey === '-') { lastKey = parentObject.length - 1; }
|
||||
parentObject.splice(lastKey, 1);
|
||||
} else if (isObject(parentObject)) {
|
||||
delete parentObject[lastKey];
|
||||
}
|
||||
return object;
|
||||
}
|
||||
console.error(`remove error: Invalid JSON Pointer: ${pointer}`);
|
||||
return object;
|
||||
}
|
||||
|
||||
/**
|
||||
* 'has' function
|
||||
*
|
||||
* Tests if an object has a value at the location specified by a JSON Pointer
|
||||
*
|
||||
* // { object } object - object to chek for value
|
||||
* // { Pointer } pointer - JSON Pointer (string or array)
|
||||
* // { boolean }
|
||||
*/
|
||||
static has(object, pointer) {
|
||||
const hasValue = this.get(object, pointer, 0, null, true);
|
||||
return hasValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 'dict' function
|
||||
*
|
||||
* Returns a (pointer -> value) dictionary for an object
|
||||
*
|
||||
* // { object } object - The object to create a dictionary from
|
||||
* // { object } - The resulting dictionary object
|
||||
*/
|
||||
static dict(object) {
|
||||
const results: any = {};
|
||||
this.forEachDeep(object, (value, pointer) => {
|
||||
if (typeof value !== 'object') { results[pointer] = value; }
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 'forEachDeep' function
|
||||
*
|
||||
* Iterates over own enumerable properties of an object or items in an array
|
||||
* and invokes an iteratee function for each key/value or index/value pair.
|
||||
* By default, iterates over items within objects and arrays after calling
|
||||
* the iteratee function on the containing object or array itself.
|
||||
*
|
||||
* The iteratee is invoked with three arguments: (value, pointer, rootObject),
|
||||
* where pointer is a JSON pointer indicating the location of the current
|
||||
* value within the root object, and rootObject is the root object initially
|
||||
* submitted to th function.
|
||||
*
|
||||
* If a third optional parameter 'bottomUp' is set to TRUE, the iterator
|
||||
* function will be called on sub-objects and arrays after being
|
||||
* called on their contents, rather than before, which is the default.
|
||||
*
|
||||
* This function can also optionally be called directly on a sub-object by
|
||||
* including optional 4th and 5th parameterss to specify the initial
|
||||
* root object and pointer.
|
||||
*
|
||||
* // { object } object - the initial object or array
|
||||
* // { (v: any, p?: string, o?: any) => any } function - iteratee function
|
||||
* // { boolean = false } bottomUp - optional, set to TRUE to reverse direction
|
||||
* // { object = object } rootObject - optional, root object or array
|
||||
* // { string = '' } pointer - optional, JSON Pointer to object within rootObject
|
||||
* // { object } - The modified object
|
||||
*/
|
||||
static forEachDeep(
|
||||
object, fn: (v: any, p?: string, o?: any) => any = (v) => v,
|
||||
bottomUp = false, pointer = '', rootObject = object
|
||||
) {
|
||||
if (typeof fn !== 'function') {
|
||||
console.error(`forEachDeep error: Iterator is not a function:`, fn);
|
||||
return;
|
||||
}
|
||||
if (!bottomUp) { fn(object, pointer, rootObject); }
|
||||
if (isObject(object) || isArray(object)) {
|
||||
for (const key of Object.keys(object)) {
|
||||
const newPointer = pointer + '/' + this.escape(key);
|
||||
this.forEachDeep(object[key], fn, bottomUp, newPointer, rootObject);
|
||||
}
|
||||
}
|
||||
if (bottomUp) { fn(object, pointer, rootObject); }
|
||||
}
|
||||
|
||||
/**
|
||||
* 'forEachDeepCopy' function
|
||||
*
|
||||
* Similar to forEachDeep, but returns a copy of the original object, with
|
||||
* the same keys and indexes, but with values replaced with the result of
|
||||
* the iteratee function.
|
||||
*
|
||||
* // { object } object - the initial object or array
|
||||
* // { (v: any, k?: string, o?: any, p?: any) => any } function - iteratee function
|
||||
* // { boolean = false } bottomUp - optional, set to TRUE to reverse direction
|
||||
* // { object = object } rootObject - optional, root object or array
|
||||
* // { string = '' } pointer - optional, JSON Pointer to object within rootObject
|
||||
* // { object } - The copied object
|
||||
*/
|
||||
static forEachDeepCopy(
|
||||
object, fn: (v: any, p?: string, o?: any) => any = (v) => v,
|
||||
bottomUp = false, pointer = '', rootObject = object
|
||||
) {
|
||||
if (typeof fn !== 'function') {
|
||||
console.error(`forEachDeepCopy error: Iterator is not a function:`, fn);
|
||||
return null;
|
||||
}
|
||||
if (isObject(object) || isArray(object)) {
|
||||
let newObject = isArray(object) ? [ ...object ] : { ...object };
|
||||
if (!bottomUp) { newObject = fn(newObject, pointer, rootObject); }
|
||||
for (const key of Object.keys(newObject)) {
|
||||
const newPointer = pointer + '/' + this.escape(key);
|
||||
newObject[key] = this.forEachDeepCopy(
|
||||
newObject[key], fn, bottomUp, newPointer, rootObject
|
||||
);
|
||||
}
|
||||
if (bottomUp) { newObject = fn(newObject, pointer, rootObject); }
|
||||
return newObject;
|
||||
} else {
|
||||
return fn(object, pointer, rootObject);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 'isJsonPointer' function
|
||||
*
|
||||
* Checks a string or array value to determine if it is a valid JSON Pointer.
|
||||
* Returns true if a string is empty, or starts with '/' or '#/'.
|
||||
* Returns true if an array contains only string values.
|
||||
*
|
||||
* // value - value to check
|
||||
* // { boolean } - true if value is a valid JSON Pointer, otherwise false
|
||||
*/
|
||||
static isJsonPointer(value) {
|
||||
if (isArray(value)) {
|
||||
return value.every(key => typeof key === 'string');
|
||||
} else if (isString(value)) {
|
||||
if (value === '' || value === '#') { return true; }
|
||||
if (value[0] === '/' || value.slice(0, 2) === '#/') {
|
||||
return !/(~[^01]|~$)/g.test(value);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 'escape' function
|
||||
*
|
||||
* Escapes a string reference key
|
||||
*
|
||||
* // { string } key - string key to escape
|
||||
* // { string } - escaped key
|
||||
*/
|
||||
static escape(key) {
|
||||
const escaped = key.toString().replace(/~/g, '~0').replace(/\//g, '~1');
|
||||
return escaped;
|
||||
}
|
||||
|
||||
/**
|
||||
* 'unescape' function
|
||||
*
|
||||
* Unescapes a string reference key
|
||||
*
|
||||
* // { string } key - string key to unescape
|
||||
* // { string } - unescaped key
|
||||
*/
|
||||
static unescape(key) {
|
||||
const unescaped = key.toString().replace(/~1/g, '/').replace(/~0/g, '~');
|
||||
return unescaped;
|
||||
}
|
||||
|
||||
/**
|
||||
* 'parse' function
|
||||
*
|
||||
* Converts a string JSON Pointer into a array of keys
|
||||
* (if input is already an an array of keys, it is returned unchanged)
|
||||
*
|
||||
* // { Pointer } pointer - JSON Pointer (string or array)
|
||||
* // { boolean = false } errors - Show error if invalid pointer?
|
||||
* // { string[] } - JSON Pointer array of keys
|
||||
*/
|
||||
static parse(pointer, errors = false) {
|
||||
if (!this.isJsonPointer(pointer)) {
|
||||
if (errors) { console.error(`parse error: Invalid JSON Pointer: ${pointer}`); }
|
||||
return null;
|
||||
}
|
||||
if (isArray(pointer)) { return <string[]>pointer; }
|
||||
if (typeof pointer === 'string') {
|
||||
if ((<string>pointer)[0] === '#') { pointer = pointer.slice(1); }
|
||||
if (<string>pointer === '' || <string>pointer === '/') { return []; }
|
||||
return (<string>pointer).slice(1).split('/').map(this.unescape);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user