mirror of
https://github.com/optim-enterprises-bv/siembol.git
synced 2025-11-17 18:35:11 +00:00
One of cleanup (#71)
* raw json obj validation, unreliable submit * fix defaults not appearing in deployment extras * version++ * disable testing when new rule or rule not valid * update version * support raw json object * cleanup * change from 'event' to 'test_specification' * version++ * test suite fix * new centrifuge prototype * fix git ignore * save changes * working version * working changes * resolve conflicts * move services, cleanup, oneOf working * clean up effects, diff bug * pr fixes * remove package lock * update gitignore
This commit is contained in:
committed by
GitHub Enterprise
parent
6baf157993
commit
e4cc7c8fd0
1
.gitignore
vendored
1
.gitignore
vendored
@@ -35,6 +35,7 @@ worker.yaml
|
||||
/dist-test
|
||||
/tmp
|
||||
/out-tsc
|
||||
package-lock.json
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
14898
config-editor/config-editor-ui/package-lock.json
generated
14898
config-editor/config-editor-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,7 @@
|
||||
"@angular/core": "^9.0.0",
|
||||
"@angular/flex-layout": "^9.0.0-beta.29",
|
||||
"@angular/forms": "^9.0.0",
|
||||
"@angular/material": "^9.0.0",
|
||||
"@angular/material": "^9.2.0",
|
||||
"@angular/platform-browser": "^9.0.0",
|
||||
"@angular/platform-browser-dynamic": "^9.0.0",
|
||||
"@angular/platform-server": "^9.0.0",
|
||||
@@ -36,10 +36,10 @@
|
||||
"@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",
|
||||
"@ngx-formly/core": "^5.5.15",
|
||||
"@ngx-formly/material": "^5.5.13",
|
||||
"@types/dragula": "^2.1.34",
|
||||
"ajv": "^6.8.1",
|
||||
"ajv": "^6.12.0",
|
||||
"core-js": "^3.6.4",
|
||||
"diff-match-patch": "^1.0.4",
|
||||
"json-ptr": "^1.2.0",
|
||||
|
||||
@@ -48,10 +48,12 @@ import { ExpansionPanelWrapperComponent } from './ngx-formly/components/expansio
|
||||
import { FormFieldWrapperComponent } from './ngx-formly/components/form-field-wrapper.component';
|
||||
import { InputTypeComponent } from './ngx-formly/components/input.type.component';
|
||||
import { JsonObjectTypeComponent } from './ngx-formly/components/json-object.type.component';
|
||||
import { UnionTypeComponent } from './ngx-formly/components/union.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 { SelectTypeComponent } from './ngx-formly/components/select.type.component';
|
||||
import { TabArrayTypeComponent } from './ngx-formly/components/tab-array.type.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';
|
||||
@@ -135,6 +137,8 @@ const DEV_PROVIDERS = [...PROD_PROVIDERS];
|
||||
HoverPopoverDirective,
|
||||
TestCaseHelpComponent,
|
||||
JsonTreeComponent,
|
||||
UnionTypeComponent,
|
||||
TabArrayTypeComponent,
|
||||
BuildInfoDialogComponent,
|
||||
],
|
||||
imports: [
|
||||
@@ -186,18 +190,20 @@ const DEV_PROVIDERS = [...PROD_PROVIDERS];
|
||||
},
|
||||
},
|
||||
{ name: 'boolean', extends: 'checkbox' },
|
||||
{ name: 'enum', component: SelectTypeComponent, wrappers: ['form-field'] },
|
||||
{ name: 'enum', extends: 'select' },
|
||||
{ name: 'null', component: NullTypeComponent, wrappers: ['form-field'] },
|
||||
{ name: 'array', component: ArrayTypeComponent },
|
||||
{ name: 'object', component: ObjectTypeComponent },
|
||||
{ name: 'tabs', component: TabsetTypeComponent},
|
||||
{ name: 'union', component: UnionTypeComponent },
|
||||
{ name: 'tab-array', component: TabArrayTypeComponent },
|
||||
],
|
||||
wrappers: [
|
||||
{ name: 'panel', component: PanelWrapperComponent },
|
||||
{ name: 'expansion-panel', component: ExpansionPanelWrapperComponent },
|
||||
{ name: 'form-field', component: FormFieldWrapperComponent },
|
||||
],
|
||||
extras: { checkExpressionOn: 'modelChange', immutable: false },
|
||||
extras: { checkExpressionOn: 'changeDetectionCheck', immutable: false },
|
||||
}),
|
||||
ReactiveFormsModule,
|
||||
FormlyMaterialModule,
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from '@angular/cdk/drag-drop';
|
||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { EditorService } from '@app/editor.service';
|
||||
import { EditorService } from '@services/editor.service';
|
||||
import { ConfigData, ConfigWrapper, Deployment, PullRequestInfo, SubmitStatus } from '@app/model';
|
||||
import { PopupService } from '@app/popup.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
@@ -155,9 +155,9 @@ export class ConfigManagerComponent implements OnInit, OnDestroy {
|
||||
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])),
|
||||
: this.editorService.configWrapper
|
||||
.unwrapOptionalsFromArrays(cloneDeep(this.filteredDeployment.configs[releaseId].configData)),
|
||||
config2: this.editorService.configWrapper.unwrapConfig(cloneDeep(this.filteredConfigs[id].configData)),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -181,8 +181,8 @@ export class ConfigManagerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
if (this.deployment.configs.find(r => r.name === item.name) === undefined) {
|
||||
const updatedDeployment: Deployment<ConfigWrapper<ConfigData>> = cloneDeep(this.deployment);
|
||||
const orderedItem: ConfigData = this.editorService.getLoader(this.serviceName).produceOrderedJson(item.configData, '/');
|
||||
item.configData = orderedItem;
|
||||
const orderedItem: ConfigData = this.editorService.configWrapper.produceOrderedJson(item.configData, '/');
|
||||
item.configData = this.editorService.configWrapper.unwrapConfig(orderedItem);
|
||||
updatedDeployment.configs.push(item);
|
||||
this.store.dispatch(new fromStore.UpdateDeployment(updatedDeployment));
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ import { StatusCode } from '@app/commons';
|
||||
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { AppConfigService } from '@app/config';
|
||||
import { EditorService } from '@app/editor.service';
|
||||
import { EditorService } from '@services/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 { FormlyJsonschema } from '@ngx-formly/core/json-schema';
|
||||
import * as fromStore from 'app/store';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { Observable } from 'rxjs';
|
||||
@@ -57,7 +57,7 @@ export class DeployDialogComponent {
|
||||
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))
|
||||
this.service.configLoader.validateRelease(data).pipe(take(1))
|
||||
.subscribe(s => {
|
||||
if (s !== undefined) {
|
||||
this.statusCode = s.status_code;
|
||||
@@ -78,7 +78,7 @@ export class DeployDialogComponent {
|
||||
}
|
||||
|
||||
private createDeploymentSchema(serviceName: string): string {
|
||||
const depSchema = this.service.getLoader(serviceName).originalSchema;
|
||||
const depSchema = this.service.configLoader.originalSchema;
|
||||
depSchema.properties[this.uiMetadata.deployment.config_array] = {};
|
||||
delete depSchema.properties[this.uiMetadata.deployment.config_array];
|
||||
delete depSchema.properties[this.uiMetadata.deployment.version];
|
||||
@@ -95,7 +95,7 @@ export class DeployDialogComponent {
|
||||
|
||||
onValidate() {
|
||||
this.deployment = {...this.deployment, ...this.extrasData};
|
||||
this.service.getLoader(this.serviceName)
|
||||
this.service.configLoader
|
||||
.validateRelease(this.deployment).pipe(take(1)).subscribe(s => {
|
||||
if (s !== undefined) {
|
||||
this.statusCode = s.status_code;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { AppConfigService } from '@app/config/app-config.service';
|
||||
import { TestCase, TestCaseMap } from '@app/model/test-case';
|
||||
import { FormlyJsonschema } from '@app/ngx-formly/formly-json-schema.service';
|
||||
|
||||
import { ChangeDetectionStrategy, OnInit } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { MatTabChangeEvent } from '@angular/material/tabs';
|
||||
@@ -34,6 +33,7 @@ export class EditorViewComponent implements OnInit {
|
||||
schema$: Observable<any> = new Observable();
|
||||
selectedConfigIndex: number = undefined;
|
||||
fields: FormlyFieldConfig[] = [];
|
||||
formlyOptions: any = {autoClear: true}
|
||||
|
||||
onClickTestCase$: Subject<MatTabChangeEvent> = new Subject();
|
||||
selectedConfigName: string;
|
||||
@@ -46,6 +46,7 @@ export class EditorViewComponent implements OnInit {
|
||||
dynamicFieldsMap$: Observable<Map<string, string>>;
|
||||
configs: ConfigWrapper<ConfigData>[];
|
||||
testCaseMap$: Observable<TestCaseMap>;
|
||||
schema: any;
|
||||
|
||||
constructor(private store: Store<fromStore.State>, private config: AppConfigService, private formlyJsonschema: FormlyJsonschema) {
|
||||
this.store.select(fromStore.getServiceName).pipe(takeUntil(this.ngUnsubscribe)).subscribe(r => {
|
||||
@@ -67,7 +68,8 @@ export class EditorViewComponent implements OnInit {
|
||||
ngOnInit() {
|
||||
this.schema$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(
|
||||
s => {
|
||||
this.fields = [this.formlyJsonschema.toFieldConfig(s.schema)];
|
||||
this.schema = s.schema;
|
||||
this.fields = [this.formlyJsonschema.toFieldConfig(cloneDeep(s.schema), this.formlyOptions)];
|
||||
this.store.dispatch(new fromStore.UpdateDynamicFieldsMap(this.formlyJsonschema.dynamicFieldsMap));
|
||||
});
|
||||
|
||||
@@ -77,6 +79,10 @@ export class EditorViewComponent implements OnInit {
|
||||
filter(f => f !== undefined),
|
||||
takeUntil(this.ngUnsubscribe)
|
||||
).subscribe(s => {
|
||||
// due to how oneOf changes the displayed fields we need to provide a freshly generated form when a rule is selected
|
||||
if (this.schema) {
|
||||
this.fields = [this.formlyJsonschema.toFieldConfig(cloneDeep(this.schema))];
|
||||
}
|
||||
this.selectedConfigName = this.configs[s].name;
|
||||
this.testCases = cloneDeep(this.testCaseMap[this.selectedConfigName]) || [];
|
||||
this.selectedConfigIndex = s;
|
||||
|
||||
@@ -3,10 +3,10 @@ import { FormGroup } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatTabChangeEvent } from '@angular/material/tabs';
|
||||
import { AppConfigService } from '@app/config';
|
||||
import { EditorService } from '@app/editor.service';
|
||||
import { EditorService } from '@services/editor.service';
|
||||
import { ConfigData, ConfigWrapper, SensorFields } from '@app/model';
|
||||
import { TEST_CASE_TAB_NAME } from '@app/model/test-case';
|
||||
import { UiMetadataMap } from '@app/model/ui-metadata-map';
|
||||
import { TEST_CASE_TAB_NAME } from '@model/test-case';
|
||||
import { UiMetadataMap } from '@model/ui-metadata-map';
|
||||
import * as JsonPointer from '@app/ngx-formly/util/jsonpointer.functions';
|
||||
import { PopupService } from '@app/popup.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
@@ -125,11 +125,10 @@ export class EditorComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private cleanConfigData(configData: ConfigData): ConfigData {
|
||||
let cfg = this.removeFieldsWhichShouldBeHidden(this.dynamicFieldsMap, configData);
|
||||
cfg = this.editorService.getLoader(this.serviceName).produceOrderedJson(cfg, '/');
|
||||
//let cfg = this.removeFieldsWhichShouldBeHidden(this.dynamicFieldsMap, configData);
|
||||
let cfg = this.editorService.configWrapper.produceOrderedJson(configData, '/');
|
||||
// recursively removes null, undefined, empty objects, empty arrays from the object
|
||||
cfg = omitEmpty(cfg);
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
@@ -148,7 +147,8 @@ export class EditorComponent implements OnInit, OnDestroy {
|
||||
configToUpdate.description = configToUpdate.configData[this.metaDataMap.description];
|
||||
configToUpdate.configData = this.cleanConfigData(configToUpdate.configData);
|
||||
// check if rule has been changed, mark unsaved
|
||||
if (JSON.stringify(this.config.configData) !== JSON.stringify(configToUpdate.configData)) {
|
||||
if (JSON.stringify(cloneDeep(this.config.configData)) !==
|
||||
JSON.stringify(configToUpdate.configData)) {
|
||||
configToUpdate.savedInBackend = false;
|
||||
}
|
||||
const newConfigs = Object.assign(this.configs.slice(), {
|
||||
|
||||
@@ -5,7 +5,7 @@ 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 { EditorService } from '@services/editor.service';
|
||||
import * as fromGuards from '../../guards';
|
||||
import { RepoResolver, ViewResolver } from '../../guards';
|
||||
import * as fromStore from '../../store';
|
||||
@@ -74,7 +74,6 @@ export class InitComponent implements OnDestroy {
|
||||
});
|
||||
this.router.resetConfig(routes);
|
||||
this.store.dispatch(new SetServiceNames(serviceList));
|
||||
this.service.createLoaders();
|
||||
})
|
||||
|
||||
this.store.select(fromStore.getServiceNames).pipe(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="viewer">
|
||||
<td-ngx-text-diff
|
||||
[left]="(leftContent === undefined ? rightContent?.configData : leftContent?.configData) | json"
|
||||
[right]="rightContent?.configData | json"
|
||||
[left]="(leftContent === undefined ? rightContent : leftContent) | json"
|
||||
[right]="rightContent | json"
|
||||
[loading]="false"
|
||||
[format]="leftContent === undefined ? 'LineByLine' : 'SideBySide'"
|
||||
[compareRowsStyle]="{'max-height': '1000px', 'overflow': 'auto'}"
|
||||
|
||||
@@ -21,8 +21,8 @@ export interface DiffContent {
|
||||
templateUrl: './json-viewer.component.html',
|
||||
})
|
||||
export class JsonViewerComponent {
|
||||
public leftContent: ConfigWrapper<ConfigData>;
|
||||
public rightContent: ConfigWrapper<ConfigData>;
|
||||
public leftContent: ConfigData;
|
||||
public rightContent: ConfigData;
|
||||
|
||||
constructor(public dialogRef: MatDialogRef<JsonViewerComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { OnInit } from '@angular/core';
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { StatusCode } from '@app/commons/status-code';
|
||||
import { EditorService } from '@app/editor.service';
|
||||
import { EditorService } from '@services/editor.service';
|
||||
import { EditorResult, ExceptionInfo } from '@app/model';
|
||||
import { TestCase } from '@app/model/test-case';
|
||||
import { ValidationState } from '@app/model/validation-status';
|
||||
|
||||
@@ -4,24 +4,24 @@ import { MatDialog } from '@angular/material/dialog';
|
||||
import { ErrorDialogComponent } from '@app/components';
|
||||
import { AppConfigService } from '@app/config';
|
||||
import { TestCase, TestCaseMap, TestCaseResultDefault, TestState } from '@app/model/test-case';
|
||||
import { FormlyJsonschema } from '@app/ngx-formly/formly-json-schema.service';
|
||||
import { PopupService } from '@app/popup.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { FormlyJsonschema } from '@app/ngx-formly/formly-json-schema.service';
|
||||
import { FormlyFieldConfig } from '@ngx-formly/core/lib/core';
|
||||
import * as fromStore from 'app/store';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import * as omitEmpty from 'omit-empty';
|
||||
import { from, Observable, of, Subject, throwError } from 'rxjs';
|
||||
import { catchError, delay, map, switchMap, take, takeUntil, tap } from 'rxjs/operators';
|
||||
import { EditorService } from '../../../editor.service';
|
||||
import { EditorService } from '@services/editor.service';
|
||||
import {
|
||||
ConfigData,
|
||||
ConfigTestDto,
|
||||
ConfigTestResult,
|
||||
ConfigWrapper,
|
||||
EditorResult,
|
||||
} from '../../../model/config-model';
|
||||
import { TestCaseWrapper, TestCaseWrapperDefault } from '../../../model/test-case';
|
||||
} from '@model/config-model';
|
||||
import { TestCaseWrapper, TestCaseWrapperDefault } from '@model/test-case';
|
||||
import { SubmitTestcaseDialogComponent } from '../submit-testcase-dialog/submit-testcase-dialog.component';
|
||||
|
||||
interface OutputDict {
|
||||
@@ -39,7 +39,7 @@ export class TestCentreComponent implements OnInit, OnDestroy {
|
||||
EVENT_HELP: string;
|
||||
private ngUnsubscribe = new Subject();
|
||||
public fields: FormlyFieldConfig[] = [];
|
||||
public options: any = {};
|
||||
public options: any = {autoClear: false};
|
||||
public testCase: TestCaseWrapper = new TestCaseWrapperDefault();
|
||||
public alert: string;
|
||||
public form: FormGroup = new FormGroup({});
|
||||
@@ -70,20 +70,23 @@ export class TestCentreComponent implements OnInit, OnDestroy {
|
||||
private dialog: MatDialog) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.service = this.editorService.getLoader(this.serviceName);
|
||||
this.store.select(fromStore.getConfigTestingEvent).pipe(take(1)).subscribe(e => this.alert = e);
|
||||
if (this.appConfig.getUiMetadata(this.serviceName).testing.testCaseEnabled) {
|
||||
this.store.select(fromStore.getTestCaseSchema).pipe(takeUntil(this.ngUnsubscribe)).subscribe(s => {
|
||||
this.service.getTestSpecificationSchema().pipe(takeUntil(this.ngUnsubscribe)).subscribe(l => {
|
||||
this.editorService.configLoader.getTestSpecificationSchema().pipe(takeUntil(this.ngUnsubscribe)).subscribe(l => {
|
||||
this.options.formState = {
|
||||
mainModel: this.testCase,
|
||||
rawObjects: {},
|
||||
|
||||
};
|
||||
const subschema = new FormlyJsonschema().toFieldConfig(l);
|
||||
const schemaConverter = new FormlyJsonschema();
|
||||
schemaConverter.testSpec = subschema;
|
||||
this.fields = [schemaConverter.toFieldConfig(s, this.options)];
|
||||
this.fields = [schemaConverter.toFieldConfig(cloneDeep(s), this.options)];
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.selectedConfigIndex$.pipe(takeUntil(this.ngUnsubscribe)).subscribe(s => {
|
||||
this.addNewTest = false;
|
||||
this.changeDetector.markForCheck();
|
||||
@@ -241,7 +244,7 @@ export class TestCentreComponent implements OnInit, OnDestroy {
|
||||
test_specification: JSON.parse(JSON.stringify(testcase.test_specification, this.replacer)),
|
||||
}
|
||||
|
||||
return this.editorService.getLoader(this.serviceName).testSingleConfig(testDto).pipe(
|
||||
return this.editorService.configLoader.testSingleConfig(testDto).pipe(
|
||||
map((r: EditorResult<ConfigTestResult>) => {
|
||||
this.output[testcase.test_case_name] = r.attributes;
|
||||
if (r.status_code === 'OK') {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Inject } from '@angular/core';
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { AppConfigService } from '@app/config';
|
||||
import { EditorService } from '@app/editor.service';
|
||||
import { EditorService } from '@services/editor.service';
|
||||
import { ConfigData, Deployment, EditorResult } from '@app/model';
|
||||
import { PopupService } from '@app/popup.service';
|
||||
import { Store } from '@ngrx/store';
|
||||
@@ -23,7 +23,6 @@ export class TestingDialogComponent implements OnDestroy, OnInit {
|
||||
EVENT_HELP: string;
|
||||
testSuccess = 'none';
|
||||
private ngUnsubscribe = new Subject();
|
||||
private service: any;
|
||||
private env: string;
|
||||
isSingleConfig: boolean;
|
||||
|
||||
@@ -35,7 +34,6 @@ export class TestingDialogComponent implements OnDestroy, OnInit {
|
||||
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;
|
||||
@@ -84,9 +82,9 @@ export class TestingDialogComponent implements OnDestroy, OnInit {
|
||||
[this.appConfig.getUiMetadata(this.env).testing.eventName]: event,
|
||||
};
|
||||
|
||||
if (this.service) {
|
||||
if (this.editorService.configLoader) {
|
||||
if (this.isSingleConfig) {
|
||||
this.service.testSingleConfig(testDto).pipe(takeUntil(this.ngUnsubscribe))
|
||||
this.editorService.configLoader.testSingleConfig(testDto).pipe(takeUntil(this.ngUnsubscribe))
|
||||
.map(r => r)
|
||||
.catch(err => {
|
||||
this.ngUnsubscribe.next();
|
||||
@@ -112,7 +110,7 @@ export class TestingDialogComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
)
|
||||
} else {
|
||||
this.service.testDeploymentConfig(testDto).pipe(takeUntil(this.ngUnsubscribe))
|
||||
this.editorService.configLoader.testDeploymentConfig(testDto).pipe(takeUntil(this.ngUnsubscribe))
|
||||
.map(r => r)
|
||||
.catch(err => {
|
||||
this.ngUnsubscribe.next();
|
||||
|
||||
@@ -1,419 +0,0 @@
|
||||
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,
|
||||
Content,
|
||||
Deployment,
|
||||
DeploymentWrapper,
|
||||
EditorResult,
|
||||
ExceptionInfo,
|
||||
GitFiles,
|
||||
PullRequestInfo,
|
||||
RepositoryLinks,
|
||||
RepositoryLinksWrapper,
|
||||
SchemaInfo,
|
||||
TestSchemaInfo,
|
||||
} from './model/config-model';
|
||||
import { Field } from './model/sensor-fields';
|
||||
import { TestCase, TestCaseMap, TestCaseResultDefault, TestCaseWrapper, TestState } from './model/test-case';
|
||||
import { UiMetadataMap } from './model/ui-metadata-map';
|
||||
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
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 serviceName: string) {
|
||||
this.uiMetadata = this.config.getUiMetadata(this.serviceName);
|
||||
try {
|
||||
this.labelsFunc = new Function('model', this.uiMetadata.labelsFunc);
|
||||
} catch {
|
||||
console.error('unable to parse labels function');
|
||||
this.labelsFunc = () => [];
|
||||
}
|
||||
}
|
||||
|
||||
public getConfigs(): Observable<ConfigWrapper<ConfigData>[]> {
|
||||
|
||||
return this.http.get<EditorResult<GitFiles<any>>>(
|
||||
`${this.config.serviceRoot}api/v1/${this.serviceName}/configstore/configs`).pipe(
|
||||
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: Content<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 getTestSpecificationSchema(): Observable<any> {
|
||||
return this.http.get<EditorResult<TestSchemaInfo>>(`${this.config.serviceRoot}api/v1/${this.serviceName}/configs/testschema`).pipe(
|
||||
map(x =>
|
||||
x.attributes.test_schema
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public getSchema(): Observable<SchemaDto> {
|
||||
return this.http.get<EditorResult<SchemaInfo>>(`${this.config.serviceRoot}api/v1/${this.serviceName}/configs/schema`).pipe(
|
||||
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);
|
||||
if (thingy.default) {
|
||||
delete thingy.default;
|
||||
}
|
||||
const sub = {...thingy};
|
||||
thingy.type = 'array';
|
||||
delete thingy.required;
|
||||
delete thingy.properties;
|
||||
delete thingy.title;
|
||||
delete thingy.description;
|
||||
|
||||
// tabs is not compatible with the array type so delete it if it is at the parent level but keep it on the sub level
|
||||
if (sub['x-schema-form'] !== undefined && sub['x-schema-form']['type'] !== 'tabs') {
|
||||
delete sub['x-schema-form'];
|
||||
}
|
||||
if (thingy['x-schema-form'] !== undefined && thingy['x-schema-form']['type'] === 'tabs' && thingy['type'] === 'array') {
|
||||
delete thingy['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.serviceName}/configstore/release/status`)
|
||||
.pipe(
|
||||
map(result => result.attributes)
|
||||
)
|
||||
}
|
||||
|
||||
public getRelease(): Observable<DeploymentWrapper> {
|
||||
return this.http.get<EditorResult<GitFiles<any>>>
|
||||
(`${this.config.serviceRoot}api/v1/${this.serviceName}/configstore/release`).pipe(
|
||||
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.serviceName}/configstore/repositories`).pipe(
|
||||
map(result => ({
|
||||
...result.attributes.rules_repositories,
|
||||
rulesetName: this.serviceName,
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
public getTestCases(): Observable<TestCaseMap> {
|
||||
return this.http.get<EditorResult<GitFiles<TestCase>>>(`${this.config.serviceRoot}api/v1/${this.serviceName}/configstore/testcases`)
|
||||
.pipe(
|
||||
map(result => this.testCaseFilesToMap(result))
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
private testCaseFilesToMap(result: EditorResult<GitFiles<TestCase>>): TestCaseMap {
|
||||
const testCaseMap: TestCaseMap = {};
|
||||
if (result.attributes && result.attributes.files && result.attributes.files.length > 0) {
|
||||
result.attributes.files.forEach(file => {
|
||||
if (!testCaseMap.hasOwnProperty(file.content.config_name)) {
|
||||
testCaseMap[file.content.config_name] = [];
|
||||
}
|
||||
const testCase: TestCaseWrapper = {
|
||||
testCase: file.content,
|
||||
testState: TestState.NOT_RUN,
|
||||
testResult: new TestCaseResultDefault(),
|
||||
fileHistory: file.file_history,
|
||||
}
|
||||
testCaseMap[file.content.config_name].push(testCase);
|
||||
});
|
||||
}
|
||||
|
||||
return testCaseMap;
|
||||
}
|
||||
|
||||
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.serviceName}/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.serviceName}/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.serviceName}/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.serviceName}/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.serviceName}/configstore/configs`, json);
|
||||
}
|
||||
|
||||
public getFields(): Observable<Field[]> {
|
||||
return this.http.get<EditorResult<any>>(
|
||||
`${this.config.serviceRoot}api/v1/${this.serviceName}/configs/fields`).pipe(
|
||||
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.serviceName}/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.serviceName}/configs/test?singleConfig=true`, testDto)
|
||||
}
|
||||
|
||||
public submitTestCaseEdit(testCase: TestCaseWrapper): Observable<TestCaseMap> {
|
||||
const json = JSON.stringify(cloneDeep(testCase.testCase), null, 2);
|
||||
|
||||
return this.http.put<EditorResult<GitFiles<TestCase>>>(
|
||||
`${this.config.serviceRoot}api/v1/${this.serviceName}/configstore/testcases`, json).pipe(
|
||||
map(result => this.testCaseFilesToMap(result))
|
||||
);
|
||||
}
|
||||
|
||||
public submitNewTestCase(testCase: TestCaseWrapper): Observable<TestCaseMap> {
|
||||
const json = JSON.stringify(cloneDeep(testCase.testCase), null, 2);
|
||||
|
||||
return this.http.post<EditorResult<GitFiles<TestCase>>>(
|
||||
`${this.config.serviceRoot}api/v1/${this.serviceName}/configstore/testcases`, json).pipe(
|
||||
map(result => this.testCaseFilesToMap(result))
|
||||
);
|
||||
}
|
||||
|
||||
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))),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NgModule, Optional, SkipSelf } from '@angular/core';
|
||||
import { EditorService } from 'app/editor.service';
|
||||
import { EditorService } from '@services/editor.service';
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface UiMetadataMap {
|
||||
enableSensorFields: boolean,
|
||||
perConfigSchemaPath: string,
|
||||
deployment: DeploymentConfig,
|
||||
unionType?: UnionType,
|
||||
}
|
||||
|
||||
export interface TestConfig {
|
||||
@@ -24,3 +25,8 @@ export interface DeploymentConfig {
|
||||
config_array: string,
|
||||
extras?: string[],
|
||||
}
|
||||
|
||||
export interface UnionType {
|
||||
unionPath: string;
|
||||
unionSelectorName: string;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { FieldArrayType } from '@ngx-formly/core';
|
||||
import { clone, isNullOrUndefined, assignModelValue, getKeyPath } from '../util/utility.functions';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
@Component({
|
||||
// tslint:disable-next-line:component-selector
|
||||
@@ -120,9 +122,26 @@ export class ArrayTypeComponent extends FieldArrayType {
|
||||
}
|
||||
}
|
||||
|
||||
reorder (oldIndex, newIndex) {
|
||||
reorder (oldIndex: number, newIndex: number) {
|
||||
const temp = this.model[oldIndex];
|
||||
this.remove(oldIndex);
|
||||
this.add(newIndex, temp);
|
||||
}
|
||||
|
||||
add(i?: number, initialModel?: any, { markAsDirty } = { markAsDirty: true }) {
|
||||
i = isNullOrUndefined(i) ? this.field.fieldGroup.length : i;
|
||||
if (!this.model) {
|
||||
assignModelValue(this.field.parent.model, getKeyPath(this.field), []);
|
||||
}
|
||||
let originalModel = cloneDeep(this.model);
|
||||
|
||||
this.model.splice(i, 0, initialModel ? clone(initialModel) : undefined);
|
||||
|
||||
(<any> this.options)._buildForm(true);
|
||||
originalModel.push(this.model[this.model.length - 1]);
|
||||
for (let i=0; i < this.model.length; i++) {
|
||||
this.model[i] = originalModel[i];
|
||||
}
|
||||
markAsDirty && this.formControl.markAsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { FieldArrayType } from '@ngx-formly/core';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { clone, isNullOrUndefined, assignModelValue, getKeyPath } from '../util/utility.functions';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'formly-tab-array',
|
||||
template: `
|
||||
<div>
|
||||
<mat-tab-group animationDuration="0ms" [(selectedIndex)]="selectedIndex">
|
||||
<mat-tab *ngFor="let tab of field.fieldGroup; index as i" [label]="getEvaluatorType(tab?.model)">
|
||||
<span class="align-right">
|
||||
<svg
|
||||
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>
|
||||
</span>
|
||||
<formly-field [field]="tab"></formly-field>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
<div class="align-right">
|
||||
<button mat-raised-button color="primary" (click)="add(selectedIndex + 1)">add evaluator</button>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.close-button {
|
||||
cursor: pointer;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
fill: orange;
|
||||
z-index: 500;
|
||||
}
|
||||
|
||||
.move-arrow {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.greyed-out {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
font-size: 18px;
|
||||
color: #707070;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.align-right {
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
display: table;
|
||||
padding-top: 5px;
|
||||
}
|
||||
`],
|
||||
})
|
||||
export class TabArrayTypeComponent extends FieldArrayType {
|
||||
public selectedIndex =0;
|
||||
|
||||
getEvaluatorType(model) {
|
||||
return Object.keys(model)[0];
|
||||
}
|
||||
|
||||
add(i?: number, initialModel?: any, { markAsDirty } = { markAsDirty: true }) {
|
||||
i = isNullOrUndefined(i) ? this.field.fieldGroup.length : i;
|
||||
if (!this.model) {
|
||||
assignModelValue(this.field.parent.model, getKeyPath(this.field), []);
|
||||
}
|
||||
let originalModel = cloneDeep(this.model);
|
||||
|
||||
this.model.splice(i, 0, initialModel ? clone(initialModel) : undefined);
|
||||
|
||||
(<any> this.options)._buildForm(true);
|
||||
originalModel.splice(i, 0, this.model[this.model.length - 1]);
|
||||
for (let i=0; i < this.model.length; i++) {
|
||||
this.model[i] = originalModel[i];
|
||||
}
|
||||
markAsDirty && this.formControl.markAsDirty();
|
||||
this.selectedIndex = i;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { FieldType } from '@ngx-formly/core';
|
||||
|
||||
@Component({
|
||||
selector: 'formly-union-type',
|
||||
template: `
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<legend *ngIf="to.label">{{ to.label }}</legend>
|
||||
<p *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>
|
||||
<formly-field *ngFor="let f of field.fieldGroup" [field]="f"></formly-field>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
export class UnionTypeComponent extends FieldType {}
|
||||
@@ -1,11 +1,13 @@
|
||||
import { TitleCasePipe } from '@angular/common';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AbstractControl } from '@angular/forms';
|
||||
import { AbstractControl, FormControl } from '@angular/forms';
|
||||
import { FormlyFieldConfig } from '@ngx-formly/core';
|
||||
import { ɵreverseDeepMerge as reverseDeepMerge } from '@ngx-formly/core';
|
||||
import { FormlyFieldConfigCache } from '@ngx-formly/core/lib/models';
|
||||
import { JSONSchema7, JSONSchema7TypeName } from 'json-schema';
|
||||
import { cloneDeep } from 'lodash';
|
||||
|
||||
|
||||
export interface FormlyJsonschemaOptions {
|
||||
/**
|
||||
* allows to intercept the mapping, taking the already mapped
|
||||
@@ -15,12 +17,72 @@ export interface FormlyJsonschemaOptions {
|
||||
map?: (mappedField: FormlyFieldConfig, mapSource: JSONSchema7) => FormlyFieldConfig;
|
||||
}
|
||||
|
||||
function isEmpty(v) {
|
||||
export function getFieldInitialValue(field: FormlyFieldConfig) {
|
||||
let value = field.options['_initialModel'];
|
||||
let paths = getKeyPath(field);
|
||||
while (field.parent) {
|
||||
field = field.parent;
|
||||
paths = [...getKeyPath(field), ...paths];
|
||||
}
|
||||
|
||||
for (const path of paths) {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
value = value[path];
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function getKeyPath(field: FormlyFieldConfigCache): string[] {
|
||||
if (!field.key) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/* We store the keyPath in the field for performance reasons. This function will be called frequently. */
|
||||
if (!field._keyPath || field._keyPath.key !== field.key) {
|
||||
const key =
|
||||
field.key.indexOf('[') === -1
|
||||
? field.key
|
||||
: field.key.replace(/\[(\w+)\]/g, '.$1');
|
||||
|
||||
field._keyPath = {
|
||||
key: field.key,
|
||||
path: key.indexOf('.') !== -1 ? key.split('.') : [key]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function isEmpty(v: any) {
|
||||
return v === '' || v === undefined || v === null;
|
||||
}
|
||||
|
||||
function isConst(schema: JSONSchema7) {
|
||||
return schema.hasOwnProperty('const') || (schema.enum && schema.enum.length === 1);
|
||||
}
|
||||
|
||||
function isEmptyFieldModel(field: FormlyFieldConfig): boolean {
|
||||
if (field.key && !field.fieldGroup) {
|
||||
return getFieldInitialValue(field) === undefined;
|
||||
}
|
||||
|
||||
return field.fieldGroup.every(f => isEmptyFieldModel(f));
|
||||
}
|
||||
|
||||
|
||||
|
||||
function isFieldValid(field: FormlyFieldConfig): boolean {
|
||||
if (field.key) {
|
||||
return field.formControl.valid;
|
||||
}
|
||||
|
||||
return field.fieldGroup.every(f => isFieldValid(f));
|
||||
}
|
||||
|
||||
interface IOptions extends FormlyJsonschemaOptions {
|
||||
schema: JSONSchema7;
|
||||
autoClear?: boolean;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@@ -30,22 +92,15 @@ export class FormlyJsonschema {
|
||||
titleCasePipe: TitleCasePipe = new TitleCasePipe();
|
||||
|
||||
constructor() {}
|
||||
|
||||
toFieldConfig(schema: JSONSchema7, options?: FormlyJsonschemaOptions): FormlyFieldConfig {
|
||||
this.dynamicFieldsMap = new Map<string, string>();
|
||||
const fieldConfig = this._toFieldConfig(schema, { schema, ...(options || {}) }, []);
|
||||
const fieldConfig = this._toFieldConfig(schema, { schema, ...options }, []);
|
||||
|
||||
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);
|
||||
}
|
||||
schema = this.resolveSchema(schema, options);
|
||||
|
||||
let field: FormlyFieldConfig = {
|
||||
type: this.guessType(schema),
|
||||
@@ -57,9 +112,14 @@ export class FormlyJsonschema {
|
||||
},
|
||||
};
|
||||
|
||||
field['autoClear'] = true;
|
||||
if (options.autoClear === false) {
|
||||
field['autoClear'] = false;
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case 'null': {
|
||||
this.addValidator(field, 'null', c => c.value === null);
|
||||
this.addValidator(field, 'null', ({ value }) => value === null);
|
||||
break;
|
||||
}
|
||||
case 'boolean': {
|
||||
@@ -81,21 +141,26 @@ export class FormlyJsonschema {
|
||||
|
||||
if (schema.hasOwnProperty('exclusiveMinimum')) {
|
||||
field.templateOptions.exclusiveMinimum = schema.exclusiveMinimum;
|
||||
this.addValidator(field, 'exclusiveMinimum', c => isEmpty(c.value) || (c.value > schema.exclusiveMinimum));
|
||||
this.addValidator(field, 'exclusiveMinimum', ({ value }) => isEmpty(value) || (value > schema.exclusiveMinimum));
|
||||
}
|
||||
|
||||
if (schema.hasOwnProperty('exclusiveMaximum')) {
|
||||
field.templateOptions.exclusiveMaximum = schema.exclusiveMaximum;
|
||||
this.addValidator(field, 'exclusiveMaximum', c => isEmpty(c.value) || (c.value < schema.exclusiveMaximum));
|
||||
this.addValidator(field, 'exclusiveMaximum', ({ value }) => isEmpty(value) || (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));
|
||||
this.addValidator(field, 'multipleOf', ({ value }) => isEmpty(value) || (value % schema.multipleOf === 0));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'string': {
|
||||
const schemaType = schema.type as JSONSchema7TypeName;
|
||||
if (Array.isArray(schemaType) && (schemaType.indexOf('null') !== -1)) {
|
||||
field.parsers = [v => isEmpty(v) ? null : v];
|
||||
}
|
||||
|
||||
['minLength', 'maxLength', 'pattern'].forEach(prop => {
|
||||
if (schema.hasOwnProperty(prop)) {
|
||||
field.templateOptions[prop] = schema[prop];
|
||||
@@ -112,7 +177,7 @@ export class FormlyJsonschema {
|
||||
const [propDeps, schemaDeps] = this.resolveDependencies(schema);
|
||||
|
||||
// TODO remove hard coded logic for generating the subschema
|
||||
if (schema.properties === undefined) {
|
||||
if (schema.properties === undefined && !schema.hasOwnProperty('oneOf')) {
|
||||
if (this.testSpec !== undefined) {
|
||||
field = this.testSpec;
|
||||
break;
|
||||
@@ -132,62 +197,129 @@ export class FormlyJsonschema {
|
||||
if (Array.isArray(schema.required) && schema.required.indexOf(key) !== -1) {
|
||||
f.templateOptions.required = true;
|
||||
}
|
||||
if (!f.templateOptions.required && propDeps[key]) {
|
||||
if (f.templateOptions && !f.templateOptions.required && propDeps[key]) {
|
||||
f.expressionProperties = {
|
||||
'templateOptions.required': m => m && propDeps[key].some(k => !isEmpty(m[k])),
|
||||
};
|
||||
}
|
||||
|
||||
if (schemaDeps[key]) {
|
||||
const getConstValue = (s: JSONSchema7) => {
|
||||
return s.hasOwnProperty('const') ? s.const : s.enum[0];
|
||||
};
|
||||
|
||||
const oneOfSchema = schemaDeps[key].oneOf;
|
||||
if (
|
||||
oneOfSchema
|
||||
&& oneOfSchema.every(o => o.properties && o.properties[key] && isConst(o.properties[key]))
|
||||
) {
|
||||
oneOfSchema.forEach(oneOfSchemaItem => {
|
||||
const { [key]: constSchema, ...properties } = oneOfSchemaItem.properties;
|
||||
field.fieldGroup.push({
|
||||
...this._toFieldConfig(schemaDeps[key], options),
|
||||
...this._toFieldConfig({ ...oneOfSchemaItem, properties }, { ...options, autoClear: true }, newPropKey),
|
||||
hideExpression: m => !m || getConstValue(constSchema) !== m[key],
|
||||
});
|
||||
});
|
||||
} else {
|
||||
field.fieldGroup.push({
|
||||
...this._toFieldConfig(schemaDeps[key], options, newPropKey),
|
||||
hideExpression: m => !m || isEmpty(m[key]),
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
if (schema.oneOf) {
|
||||
field.fieldGroup.push(this.resolveMultiSchema(
|
||||
'oneOf',
|
||||
<JSONSchema7[]> schema.oneOf,
|
||||
options,
|
||||
cloneDeep(propKey)
|
||||
));
|
||||
}
|
||||
|
||||
if (schema.anyOf) {
|
||||
field.fieldGroup.push(this.resolveMultiSchema(
|
||||
'anyOf',
|
||||
<JSONSchema7[]> schema.anyOf,
|
||||
options,
|
||||
cloneDeep(propKey)
|
||||
));
|
||||
}
|
||||
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));
|
||||
this.addValidator(field, 'minItems', ({ value }) => isEmpty(value) || (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));
|
||||
this.addValidator(field, 'maxItems', ({ value }) => isEmpty(value) || (value.length <= schema.maxItems));
|
||||
}
|
||||
if (schema.hasOwnProperty('uniqueItems')) {
|
||||
field.templateOptions.uniqueItems = schema.uniqueItems;
|
||||
this.addValidator(field, 'uniqueItems', ({ value }) => {
|
||||
if (isEmpty(value) || !schema.uniqueItems) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const uniqueItems = Array.from(
|
||||
new Set(value.map((v: any) => JSON.stringify(v))),
|
||||
);
|
||||
|
||||
return uniqueItems.length === value.length;
|
||||
});
|
||||
}
|
||||
|
||||
// resolve items schema needed for isEnum check
|
||||
if (schema.items && !Array.isArray(schema.items)) {
|
||||
schema.items = this.resolveSchema(<JSONSchema7> schema.items, options);
|
||||
}
|
||||
|
||||
// TODO: remove isEnum check once adding an option to skip extension
|
||||
if (!this.isEnum(schema)) {
|
||||
const _this = this;
|
||||
Object.defineProperty(field, 'fieldArray', {
|
||||
get: () => {
|
||||
get: function() {
|
||||
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);
|
||||
return _this._toFieldConfig(<JSONSchema7> schema.items, options, newPropKey2);
|
||||
}
|
||||
|
||||
const itemSchema = schema.items[field.fieldGroup.length]
|
||||
? schema.items[field.fieldGroup.length]
|
||||
const length = this.fieldGroup ? this.fieldGroup.length : 0;
|
||||
const itemSchema = schema.items[length]
|
||||
? schema.items[length]
|
||||
: schema.additionalItems;
|
||||
|
||||
return itemSchema
|
||||
? this._toFieldConfig(<JSONSchema7> itemSchema, options)
|
||||
: null;
|
||||
? _this._toFieldConfig(<JSONSchema7> itemSchema, options, newPropKey2)
|
||||
: {};
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.hasOwnProperty('const')) {
|
||||
field.templateOptions.const = schema.const;
|
||||
this.addValidator(field, 'const', ({ value }) => value === schema.const);
|
||||
if (!field.type) {
|
||||
field.defaultValue = schema.const;
|
||||
}
|
||||
}
|
||||
if (schema.hasOwnProperty('x-schema-form')) {
|
||||
if (schema['x-schema-form'].hasOwnProperty('type')) {
|
||||
field.type = schema['x-schema-form'].type;
|
||||
@@ -206,14 +338,20 @@ export class FormlyJsonschema {
|
||||
} catch {
|
||||
console.warn('Something went wrong with applying condition evaluation to form');
|
||||
}
|
||||
if (!schema['x-schema-form'].condition.hasOwnProperty('isContant')) {
|
||||
this.dynamicFieldsMap.set(('/' + propKey.join('/')), schema['x-schema-form'].condition.hideExpression);
|
||||
}
|
||||
}
|
||||
if (schema['x-schema-form'].condition.hasOwnProperty('disableAutoClear')) {
|
||||
field['autoClear'] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.enum) {
|
||||
if (this.isEnum(schema)) {
|
||||
field.templateOptions.multiple = field.type === 'array';
|
||||
field.type = 'enum';
|
||||
field.templateOptions.options = schema.enum.map(value => ({ value, label: value }));
|
||||
field.templateOptions.options = this.toEnumOptions(schema);
|
||||
}
|
||||
|
||||
// map in possible formlyConfig options from the widget property
|
||||
@@ -226,12 +364,7 @@ export class FormlyJsonschema {
|
||||
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) => {
|
||||
private resolveSchema(schema: JSONSchema7, options: IOptions) {
|
||||
if (schema.$ref) {
|
||||
schema = this.resolveDefinition(schema, options);
|
||||
}
|
||||
@@ -239,6 +372,17 @@ export class FormlyJsonschema {
|
||||
if (schema.allOf) {
|
||||
schema = this.resolveAllOf(schema, options);
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
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) => {
|
||||
schema = this.resolveSchema(schema, options);
|
||||
if (base.required && schema.required) {
|
||||
base.required = [...base.required, ...schema.required];
|
||||
}
|
||||
@@ -267,6 +411,60 @@ export class FormlyJsonschema {
|
||||
}, baseSchema);
|
||||
}
|
||||
|
||||
private resolveMultiSchema(
|
||||
mode: 'oneOf' | 'anyOf',
|
||||
schemas: JSONSchema7[],
|
||||
options: IOptions,
|
||||
propKey: string[]
|
||||
): FormlyFieldConfig {
|
||||
return {
|
||||
type: 'union',
|
||||
fieldGroup: [
|
||||
{
|
||||
type: 'enum',
|
||||
templateOptions: {
|
||||
multiple: mode === 'anyOf',
|
||||
options: schemas
|
||||
.map((s, i) => ({ label: s.title, value: i })),
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldGroup: schemas.map((s, i) => ({
|
||||
...this._toFieldConfig(s, { ...options, autoClear: true }, propKey),
|
||||
hideExpression: (m, fs, f) => {
|
||||
const selectField = f.parent.parent.fieldGroup[0];
|
||||
if (!selectField.formControl) {
|
||||
const value = f.parent.fieldGroup
|
||||
.map((f, i) => [f, i] as [FormlyFieldConfig, number])
|
||||
.filter(([f]) => isFieldValid(f))
|
||||
.sort(([f1], [f2]) => {
|
||||
const isDefaultModel = isEmptyFieldModel(f1);
|
||||
if (isDefaultModel === isEmptyFieldModel(f2)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return isDefaultModel ? 1 : -1;
|
||||
})
|
||||
.map(([, i]) => i)
|
||||
;
|
||||
|
||||
const normalizedValue = [value.length === 0 ? 0 : value[0]];
|
||||
const formattedValue = mode === 'anyOf' ? normalizedValue : normalizedValue[0];
|
||||
selectField.formControl = new FormControl(formattedValue);
|
||||
}
|
||||
|
||||
const control = selectField.formControl;
|
||||
|
||||
return Array.isArray(control.value)
|
||||
? control.value.indexOf(i) === -1
|
||||
: control.value !== i;
|
||||
},
|
||||
})),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
private resolveDefinition(schema: JSONSchema7, options: IOptions): JSONSchema7 {
|
||||
const [uri, pointer] = schema.$ref.split('#/');
|
||||
if (uri) {
|
||||
@@ -275,7 +473,7 @@ export class FormlyJsonschema {
|
||||
|
||||
const definition = !pointer ? null : pointer.split('/').reduce(
|
||||
(def, path) => def && def.hasOwnProperty(path) ? def[path] : null,
|
||||
options.schema
|
||||
options.schema,
|
||||
);
|
||||
|
||||
if (!definition) {
|
||||
@@ -328,6 +526,16 @@ export class FormlyJsonschema {
|
||||
return 'object';
|
||||
}
|
||||
|
||||
if (Array.isArray(type)) {
|
||||
if (type.length === 1) {
|
||||
return type[0];
|
||||
}
|
||||
|
||||
if (type.length === 2 && type.indexOf('null') !== -1) {
|
||||
return type[type[0] === 'null' ? 1 : 0];
|
||||
}
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
@@ -335,4 +543,33 @@ export class FormlyJsonschema {
|
||||
field.validators = field.validators || {};
|
||||
field.validators[name] = validator;
|
||||
}
|
||||
|
||||
private isEnum(schema: JSONSchema7) {
|
||||
return schema.enum
|
||||
|| (schema.anyOf && schema.anyOf.every(isConst))
|
||||
|| (schema.oneOf && schema.oneOf.every(isConst))
|
||||
|| schema.uniqueItems && schema.items && !Array.isArray(schema.items) && this.isEnum(<JSONSchema7> schema.items);
|
||||
}
|
||||
|
||||
private toEnumOptions(schema: JSONSchema7) {
|
||||
if (schema.enum) {
|
||||
return schema.enum.map(value => ({ value, label: value }));
|
||||
}
|
||||
|
||||
const toEnum = (s: JSONSchema7) => {
|
||||
const value = s.hasOwnProperty('const') ? s.const : s.enum[0];
|
||||
|
||||
return { value: value, label: s.title || value };
|
||||
};
|
||||
|
||||
if (schema.anyOf) {
|
||||
return schema.anyOf.map(toEnum);
|
||||
}
|
||||
|
||||
if (schema.oneOf) {
|
||||
return schema.oneOf.map(toEnum);
|
||||
}
|
||||
|
||||
return this.toEnumOptions(<JSONSchema7> schema.items);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { isObservable } from 'rxjs';
|
||||
|
||||
import {
|
||||
hasValue,
|
||||
inArray,
|
||||
@@ -10,6 +12,8 @@ import {
|
||||
isString,
|
||||
PlainObject
|
||||
} from './validator.functions';
|
||||
import { AbstractControl } from '@angular/forms';
|
||||
import { FormlyFieldConfigCache } from '@ngx-formly/core/lib/components/formly.field.config';
|
||||
|
||||
/**
|
||||
* Utility function library:
|
||||
@@ -318,3 +322,88 @@ export function toTitleCase(input: string, forceWords?: string | string[]): stri
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function getKeyPath(field: FormlyFieldConfigCache): string[] {
|
||||
if (!field.key) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/* We store the keyPath in the field for performance reasons. This function will be called frequently. */
|
||||
if (!field._keyPath || field._keyPath.key !== field.key) {
|
||||
const key = field.key.indexOf('[') === -1
|
||||
? field.key
|
||||
: field.key.replace(/\[(\w+)\]/g, '.$1');
|
||||
|
||||
field._keyPath = { key: field.key, path: key.indexOf('.') !== -1 ? key.split('.') : [key] };
|
||||
}
|
||||
|
||||
return field._keyPath.path.slice(0);
|
||||
}
|
||||
|
||||
export function assignModelValue(model: any, paths: string[], value: any) {
|
||||
for (let i = 0; i < (paths.length - 1); i++) {
|
||||
const path = paths[i];
|
||||
if (!model[path] || !isObject(model[path])) {
|
||||
model[path] = /^\d+$/.test(paths[i + 1]) ? [] : {};
|
||||
}
|
||||
|
||||
model = model[path];
|
||||
}
|
||||
|
||||
model[paths[paths.length - 1]] = clone(value);
|
||||
}
|
||||
|
||||
export function clone(value: any): any {
|
||||
if (
|
||||
!isObject(value)
|
||||
|| isObservable(value)
|
||||
|| /* instanceof SafeHtmlImpl */ value.changingThisBreaksApplicationSecurity
|
||||
|| ['RegExp', 'FileList', 'File', 'Blob'].indexOf(value.constructor.name) !== -1
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// https://github.com/moment/moment/blob/master/moment.js#L252
|
||||
if (value._isAMomentObject && isFunction(value.clone)) {
|
||||
return value.clone();
|
||||
}
|
||||
|
||||
if (value instanceof AbstractControl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
return new Date(value.getTime());
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.slice(0).map(v => clone(v));
|
||||
}
|
||||
|
||||
// best way to clone a js object maybe
|
||||
// https://stackoverflow.com/questions/41474986/how-to-clone-a-javascript-es6-class-instance
|
||||
const proto = Object.getPrototypeOf(value);
|
||||
let c = Object.create(proto);
|
||||
c = Object.setPrototypeOf(c, proto);
|
||||
// need to make a deep copy so we dont use Object.assign
|
||||
// also Object.assign wont copy property descriptor exactly
|
||||
return Object.keys(value).reduce((newVal, prop) => {
|
||||
const propDesc = Object.getOwnPropertyDescriptor(value, prop);
|
||||
if (propDesc.get) {
|
||||
Object.defineProperty(newVal, prop, propDesc);
|
||||
} else {
|
||||
newVal[prop] = clone(value[prop]);
|
||||
}
|
||||
|
||||
return newVal;
|
||||
}, c);
|
||||
}
|
||||
|
||||
export function isNullOrUndefined(value: any) {
|
||||
return value === undefined || value === null;
|
||||
}
|
||||
|
||||
export function isFunction(value: any) {
|
||||
return typeof(value) === 'function';
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
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,
|
||||
Content,
|
||||
Deployment,
|
||||
DeploymentWrapper,
|
||||
EditorResult,
|
||||
ExceptionInfo,
|
||||
GitFiles,
|
||||
PullRequestInfo,
|
||||
SchemaInfo,
|
||||
TestSchemaInfo
|
||||
} from '@model/config-model';
|
||||
import { Field } from '@model/sensor-fields';
|
||||
import {
|
||||
TestCase,
|
||||
TestCaseMap,
|
||||
TestCaseResultDefault,
|
||||
TestCaseWrapper,
|
||||
TestState
|
||||
} from '@model/test-case';
|
||||
import { UiMetadataMap } from '@model/ui-metadata-map';
|
||||
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { ConfigWrapperService } from './config-wrapper-service';
|
||||
|
||||
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 serviceName: string,
|
||||
private configWrapperService: ConfigWrapperService
|
||||
) {
|
||||
this.uiMetadata = this.config.getUiMetadata(this.serviceName);
|
||||
try {
|
||||
this.labelsFunc = new Function('model', this.uiMetadata.labelsFunc);
|
||||
} catch {
|
||||
console.error('unable to parse labels function');
|
||||
this.labelsFunc = () => [];
|
||||
}
|
||||
}
|
||||
|
||||
public getConfigs(): Observable<ConfigWrapper<ConfigData>[]> {
|
||||
return this.http
|
||||
.get<EditorResult<GitFiles<any>>>(
|
||||
`${this.config.serviceRoot}api/v1/${
|
||||
this.serviceName
|
||||
}/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.configWrapperService.wrapConfig(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: Content<any>[]
|
||||
): ConfigWrapper<ConfigData>[] {
|
||||
const ret: ConfigWrapper<ConfigData>[] = [];
|
||||
for (const file of files) {
|
||||
ret.push({
|
||||
isNew: false,
|
||||
configData: this.configWrapperService.wrapConfig(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 getTestSpecificationSchema(): Observable<any> {
|
||||
return this.http
|
||||
.get<EditorResult<TestSchemaInfo>>(
|
||||
`${this.config.serviceRoot}api/v1/${
|
||||
this.serviceName
|
||||
}/configs/testschema`
|
||||
)
|
||||
.pipe(map(x => x.attributes.test_schema));
|
||||
}
|
||||
|
||||
public getSchema(): Observable<SchemaDto> {
|
||||
return this.http
|
||||
.get<EditorResult<SchemaInfo>>(
|
||||
`${this.config.serviceRoot}api/v1/${this.serviceName}/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.configWrapperService.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 };
|
||||
});
|
||||
}
|
||||
|
||||
public getPullRequestStatus(): Observable<PullRequestInfo> {
|
||||
return this.http
|
||||
.get<EditorResult<PullRequestInfo>>(
|
||||
`${this.config.serviceRoot}api/v1/${
|
||||
this.serviceName
|
||||
}/configstore/release/status`
|
||||
)
|
||||
.pipe(map(result => result.attributes));
|
||||
}
|
||||
|
||||
public getRelease(): Observable<DeploymentWrapper> {
|
||||
return this.http
|
||||
.get<EditorResult<GitFiles<any>>>(
|
||||
`${this.config.serviceRoot}api/v1/${
|
||||
this.serviceName
|
||||
}/configstore/release`
|
||||
)
|
||||
.pipe(
|
||||
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.configWrapperService.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 getTestCases(): Observable<TestCaseMap> {
|
||||
return this.http
|
||||
.get<EditorResult<GitFiles<TestCase>>>(
|
||||
`${this.config.serviceRoot}api/v1/${
|
||||
this.serviceName
|
||||
}/configstore/testcases`
|
||||
)
|
||||
.pipe(map(result => this.testCaseFilesToMap(result)));
|
||||
}
|
||||
|
||||
private testCaseFilesToMap(
|
||||
result: EditorResult<GitFiles<TestCase>>
|
||||
): TestCaseMap {
|
||||
const testCaseMap: TestCaseMap = {};
|
||||
if (
|
||||
result.attributes &&
|
||||
result.attributes.files &&
|
||||
result.attributes.files.length > 0
|
||||
) {
|
||||
result.attributes.files.forEach(file => {
|
||||
if (!testCaseMap.hasOwnProperty(file.content.config_name)) {
|
||||
testCaseMap[file.content.config_name] = [];
|
||||
}
|
||||
const testCase: TestCaseWrapper = {
|
||||
testCase: file.content,
|
||||
testState: TestState.NOT_RUN,
|
||||
testResult: new TestCaseResultDefault(),
|
||||
fileHistory: file.file_history
|
||||
};
|
||||
testCaseMap[file.content.config_name].push(testCase);
|
||||
});
|
||||
}
|
||||
|
||||
return testCaseMap;
|
||||
}
|
||||
|
||||
public validateConfig(
|
||||
config: ConfigWrapper<ConfigData>
|
||||
): Observable<EditorResult<ExceptionInfo>> {
|
||||
const json = JSON.stringify(
|
||||
this.configWrapperService.unwrapConfig(cloneDeep(config.configData)),
|
||||
null,
|
||||
2
|
||||
);
|
||||
|
||||
return this.http.post<EditorResult<ExceptionInfo>>(
|
||||
`${this.config.serviceRoot}api/v1/${
|
||||
this.serviceName
|
||||
}/configs/validate?singleConfig=true`,
|
||||
json
|
||||
);
|
||||
}
|
||||
|
||||
public validateRelease(
|
||||
deployment: Deployment<ConfigWrapper<ConfigData>>
|
||||
): Observable<EditorResult<ExceptionInfo>> {
|
||||
const validationFormat = this.configWrapperService.marshalDeploymentFormat(
|
||||
deployment
|
||||
);
|
||||
const json = JSON.stringify(validationFormat, null, 2);
|
||||
|
||||
return this.http.post<EditorResult<ExceptionInfo>>(
|
||||
`${this.config.serviceRoot}api/v1/${this.serviceName}/configs/validate`,
|
||||
json
|
||||
);
|
||||
}
|
||||
|
||||
public submitRelease(
|
||||
deployment: Deployment<ConfigWrapper<ConfigData>>
|
||||
): Observable<EditorResult<ExceptionInfo>> {
|
||||
const releaseFormat = this.configWrapperService.marshalDeploymentFormat(
|
||||
deployment
|
||||
);
|
||||
const json = JSON.stringify(releaseFormat, null, 2);
|
||||
|
||||
return this.http.post<EditorResult<ExceptionInfo>>(
|
||||
`${this.config.serviceRoot}api/v1/${
|
||||
this.serviceName
|
||||
}/configstore/release`,
|
||||
json
|
||||
);
|
||||
}
|
||||
|
||||
public submitConfigEdit(
|
||||
config: ConfigWrapper<ConfigData>
|
||||
): Observable<EditorResult<GitFiles<ConfigData>>> {
|
||||
const json = JSON.stringify(
|
||||
this.configWrapperService.unwrapConfig(cloneDeep(config.configData)),
|
||||
null,
|
||||
2
|
||||
);
|
||||
|
||||
return this.http.put<EditorResult<GitFiles<ConfigData>>>(
|
||||
`${this.config.serviceRoot}api/v1/${
|
||||
this.serviceName
|
||||
}/configstore/configs`,
|
||||
json
|
||||
);
|
||||
}
|
||||
|
||||
public submitNewConfig(
|
||||
config: ConfigWrapper<ConfigData>
|
||||
): Observable<EditorResult<GitFiles<ConfigData>>> {
|
||||
const json = JSON.stringify(
|
||||
this.configWrapperService.unwrapConfig(cloneDeep(config.configData)),
|
||||
null,
|
||||
2
|
||||
);
|
||||
|
||||
return this.http.post<EditorResult<GitFiles<ConfigData>>>(
|
||||
`${this.config.serviceRoot}api/v1/${
|
||||
this.serviceName
|
||||
}/configstore/configs`,
|
||||
json
|
||||
);
|
||||
}
|
||||
|
||||
public getFields(): Observable<Field[]> {
|
||||
return this.http
|
||||
.get<EditorResult<any>>(
|
||||
`${this.config.serviceRoot}api/v1/${this.serviceName}/configs/fields`
|
||||
)
|
||||
.pipe(map(f => f.attributes.fields));
|
||||
}
|
||||
|
||||
public testDeploymentConfig(
|
||||
testDto: ConfigTestDto
|
||||
): Observable<EditorResult<ConfigTestResult>> {
|
||||
testDto.files[0].content = this.configWrapperService.marshalDeploymentFormat(
|
||||
testDto.files[0].content
|
||||
);
|
||||
|
||||
return this.http.post<EditorResult<any>>(
|
||||
`${this.config.serviceRoot}api/v1/${
|
||||
this.serviceName
|
||||
}/configs/test?singleConfig=false`,
|
||||
testDto
|
||||
);
|
||||
}
|
||||
|
||||
public testSingleConfig(
|
||||
testDto: ConfigTestDto
|
||||
): Observable<EditorResult<ConfigTestResult>> {
|
||||
testDto.files[0].content = this.configWrapperService.unwrapConfig(
|
||||
cloneDeep(testDto.files[0].content)
|
||||
);
|
||||
|
||||
return this.http.post<EditorResult<any>>(
|
||||
`${this.config.serviceRoot}api/v1/${
|
||||
this.serviceName
|
||||
}/configs/test?singleConfig=true`,
|
||||
testDto
|
||||
);
|
||||
}
|
||||
|
||||
public submitTestCaseEdit(
|
||||
testCase: TestCaseWrapper
|
||||
): Observable<TestCaseMap> {
|
||||
const json = JSON.stringify(cloneDeep(testCase.testCase), null, 2);
|
||||
|
||||
return this.http
|
||||
.put<EditorResult<GitFiles<TestCase>>>(
|
||||
`${this.config.serviceRoot}api/v1/${
|
||||
this.serviceName
|
||||
}/configstore/testcases`,
|
||||
json
|
||||
)
|
||||
.pipe(map(result => this.testCaseFilesToMap(result)));
|
||||
}
|
||||
|
||||
public submitNewTestCase(testCase: TestCaseWrapper): Observable<TestCaseMap> {
|
||||
const json = JSON.stringify(cloneDeep(testCase.testCase), null, 2);
|
||||
|
||||
return this.http
|
||||
.post<EditorResult<GitFiles<TestCase>>>(
|
||||
`${this.config.serviceRoot}api/v1/${
|
||||
this.serviceName
|
||||
}/configstore/testcases`,
|
||||
json
|
||||
)
|
||||
.pipe(map(result => this.testCaseFilesToMap(result)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import { UiMetadataMap } from '@model/ui-metadata-map';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { ConfigData, Deployment, ConfigWrapper } from '@app/model';
|
||||
|
||||
export class ConfigWrapperService {
|
||||
modelOrder = {};
|
||||
unionPath: string;
|
||||
optionalObjects: string[] = [];
|
||||
selectorName: string;
|
||||
|
||||
constructor(private uiMetadata: UiMetadataMap) {
|
||||
this.unionPath = uiMetadata.unionType.unionPath;
|
||||
this.selectorName = uiMetadata.unionType.unionSelectorName;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
public 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);
|
||||
if (thingy.default) {
|
||||
delete thingy.default;
|
||||
}
|
||||
const sub = {...thingy};
|
||||
thingy.type = 'array';
|
||||
delete thingy.required;
|
||||
delete thingy.properties;
|
||||
delete thingy.title;
|
||||
delete thingy.description;
|
||||
|
||||
// tabs is not compatible with the array type so delete it if it is at the parent level but keep it on the sub level
|
||||
if (sub['x-schema-form'] !== undefined && sub['x-schema-form']['type'] !== 'tabs') {
|
||||
delete sub['x-schema-form'];
|
||||
}
|
||||
if (thingy['x-schema-form'] !== undefined && thingy['x-schema-form']['type'] === 'tabs' && thingy['type'] === 'array') {
|
||||
delete thingy['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.hasOwnProperty('oneOf')) {
|
||||
this.wrapSchemaUnion(obj.items.oneOf);
|
||||
this.unionPath = path + propKey;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private wrapUnionConfig(obj, oneOfPath: string) {
|
||||
const path = oneOfPath.split("/").filter(f => f !== '');
|
||||
let sub = obj;
|
||||
for (const part of path) {
|
||||
sub = sub[part];
|
||||
}
|
||||
for (let i=0; i<sub.length; i++) {
|
||||
let temp = sub[i];
|
||||
sub[i] = {[sub[i][this.selectorName]]: temp}
|
||||
}
|
||||
}
|
||||
|
||||
public wrapOptionalsInArray(obj: object) {
|
||||
for (const optional of this.optionalObjects) {
|
||||
this.findAndWrap(obj, optional);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
public wrapConfig(obj: object): object {
|
||||
let config = this.wrapOptionalsInArray(obj);
|
||||
if (this.unionPath) {
|
||||
this.wrapUnionConfig(config, this.unionPath);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
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 unwrapConfigFromUnion(obj, oneOfPath: string): any {
|
||||
const path = oneOfPath.split("/").filter(f => f !== '');
|
||||
let sub = obj;
|
||||
for (const part of path) {
|
||||
sub = sub[part];
|
||||
}
|
||||
for (let i = 0; i < sub.length; i++) {
|
||||
let keys = Object.keys(sub[i]);
|
||||
let temp = sub[i][keys[0]];
|
||||
sub[i][keys[0]] = undefined;
|
||||
sub[i] = {...temp};
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
private wrapSchemaUnion(obj: any, propKey?: string, path?: string): any {
|
||||
for (let i=0; i<obj.length; i++) {
|
||||
let temp = obj[i].properties;
|
||||
|
||||
let required = obj[i].required;
|
||||
obj[i].properties = {[obj[i].title]: {type: 'object', properties: temp, required: required}}
|
||||
obj[i].required = [obj[i].title];
|
||||
}
|
||||
}
|
||||
|
||||
public unwrapConfig(obj: object): object {
|
||||
if (this.unionPath) {
|
||||
obj = this.unwrapConfigFromUnion(obj, this.unionPath);
|
||||
}
|
||||
return this.unwrapOptionalsFromArrays(obj);
|
||||
}
|
||||
|
||||
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 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))),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,15 @@ import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { AppConfigService } from './config';
|
||||
import { AppConfigService } from '../config';
|
||||
import { StripSuffixPipe } from '../pipes';
|
||||
import { ConfigLoaderService } from './config-loader.service';
|
||||
import { ConfigWrapperService } from './config-wrapper-service';
|
||||
import { ConfigData, ConfigWrapper, Deployment, GitFiles, PullRequestInfo, RepositoryLinks, SchemaDto, SensorFields,
|
||||
SensorFieldTemplate, UserName } from './model';
|
||||
import { ConfigTestDto, DeploymentWrapper, EditorResult, ExceptionInfo, SchemaInfo, TestCaseEvaluation } from './model/config-model';
|
||||
import { Field } from './model/sensor-fields';
|
||||
import { TestCase, TestCaseMap, TestCaseResult, TestCaseWrapper } from './model/test-case';
|
||||
import { StripSuffixPipe } from './pipes';
|
||||
SensorFieldTemplate, UserName, RepositoryLinksWrapper } from '@model';
|
||||
import { ConfigTestDto, DeploymentWrapper, EditorResult, ExceptionInfo, SchemaInfo, TestCaseEvaluation } from '@model/config-model';
|
||||
import { TestCase, TestCaseMap, TestCaseResult, TestCaseWrapper } from '@model/test-case';
|
||||
import { Field } from '@model/sensor-fields';
|
||||
|
||||
export interface IConfigLoaderService {
|
||||
originalSchema;
|
||||
@@ -18,7 +19,6 @@ export interface IConfigLoaderService {
|
||||
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>>>;
|
||||
@@ -31,8 +31,6 @@ export interface IConfigLoaderService {
|
||||
getTestCases(): Observable<TestCaseMap>;
|
||||
submitTestCaseEdit(testCase: TestCaseWrapper): Observable<TestCaseMap>;
|
||||
submitNewTestCase(testCase: TestCaseWrapper): Observable<TestCaseMap>;
|
||||
produceOrderedJson(configData: ConfigData, path: string);
|
||||
unwrapOptionalsFromArrays(obj: any);
|
||||
};
|
||||
|
||||
export function replacer(key, value) {
|
||||
@@ -45,6 +43,8 @@ export function replacer(key, value) {
|
||||
export class EditorService {
|
||||
|
||||
loaderServices: Map<string, IConfigLoaderService> = new Map();
|
||||
public configLoader: ConfigLoaderService;
|
||||
configWrapper: ConfigWrapperService;
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
@@ -79,10 +79,19 @@ export class EditorService {
|
||||
)
|
||||
}
|
||||
|
||||
public createLoaders() {
|
||||
this.config.getServiceList().forEach(element => {
|
||||
this.loaderServices.set(element, new ConfigLoaderService(this.http, this.config, element));
|
||||
});
|
||||
public getRepositoryLinks(serviceName): Observable<RepositoryLinks> {
|
||||
return this.http.get<EditorResult<RepositoryLinksWrapper>>(
|
||||
`${this.config.serviceRoot}api/v1/${serviceName}/configstore/repositories`).pipe(
|
||||
map(result => ({
|
||||
...result.attributes.rules_repositories,
|
||||
rulesetName: serviceName,
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
public createLoader(serviceName: string) {
|
||||
this.configWrapper = new ConfigWrapperService(this.config.getUiMetadata(serviceName));
|
||||
this.configLoader = new ConfigLoaderService(this.http, this.config, serviceName, this.configWrapper);
|
||||
}
|
||||
|
||||
public getTestCaseSchema(): Observable<any> {
|
||||
@@ -119,12 +128,4 @@ export class EditorService {
|
||||
map(x => x.attributes)
|
||||
)
|
||||
}
|
||||
|
||||
public getLoader(serviceName: string): IConfigLoaderService {
|
||||
try {
|
||||
return this.loaderServices.get(serviceName);
|
||||
} catch {
|
||||
throw new DOMException('Invalid service name - can\'t do nothing');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,14 @@ export const SUBMIT_NEW_TESTCASE_FAILURE = '[Testcase] Submit New Testcase Failu
|
||||
export const UPDATE_TEST_CASE_STATE = '[Testcase] Update Testcase State';
|
||||
export const UPDATE_ALL_TEST_CASE_STATE = '[Testcase] Update All Testcase State';
|
||||
|
||||
export const SET_MODEL_ORDER = '[Schema] Set Model Order';
|
||||
|
||||
|
||||
export class SetModelOrder implements Action {
|
||||
readonly type = SET_MODEL_ORDER;
|
||||
constructor(public payload: object) { }
|
||||
}
|
||||
|
||||
export class SetServiceNames implements Action {
|
||||
readonly type = SET_SERVICE_NAMES;
|
||||
constructor(public payload: string[]) { }
|
||||
@@ -180,7 +188,7 @@ export class SubmitNewConfig implements Action {
|
||||
|
||||
export class SubmitNewConfigSuccess implements Action {
|
||||
readonly type = SUBMIT_NEW_CONFIG_SUCCESS;
|
||||
constructor(public payload: ConfigWrapper<ConfigData>) { }
|
||||
constructor(public payload: ConfigWrapper<ConfigData>[]) { }
|
||||
}
|
||||
|
||||
export class SubmitNewConfigFailure implements Action {
|
||||
@@ -195,7 +203,7 @@ export class SubmitConfigEdit implements Action {
|
||||
|
||||
export class SubmitConfigEditSuccess implements Action {
|
||||
readonly type = SUBMIT_CONFIG_EDIT_SUCCESS;
|
||||
constructor(public payload: EditorResult<ConfigData>) { }
|
||||
constructor(public payload: ConfigWrapper<ConfigData>[]) { }
|
||||
}
|
||||
|
||||
export class SubmitConfigEditFailure implements Action {
|
||||
@@ -364,4 +372,5 @@ export type Actions
|
||||
| LoadTestCasesSuccess
|
||||
| LoadTestCasesFailure
|
||||
| UpdateAllTestCaseState
|
||||
| UpdateTestCaseState;
|
||||
| UpdateTestCaseState
|
||||
| SetModelOrder;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { EditorService } from '@app/editor.service';
|
||||
import { EditorService } from '@services/editor.service';
|
||||
import { Actions, Effect, ofType } from '@ngrx/effects';
|
||||
import { Action, Store } from '@ngrx/store';
|
||||
import { PopupService } from 'app/popup.service';
|
||||
@@ -26,21 +26,23 @@ export class EditorEffects {
|
||||
ofType<actions.Bootstrap>(actions.BOOTSTRAP),
|
||||
withLatestFrom(this.store.select(fromStore.getBootstrapped)),
|
||||
filter(([action, bootStrapped]) => bootStrapped !== action.payload),
|
||||
exhaustMap(([action, isBootStrapped]) => forkJoin([
|
||||
this.editorService.getLoader(action.payload).getSchema().pipe(
|
||||
tap(([action, _]) => this.editorService.createLoader(action.payload)),
|
||||
exhaustMap((_) => forkJoin([
|
||||
|
||||
this.editorService.configLoader.getSchema().pipe(
|
||||
switchMap(schema => forkJoin([
|
||||
of(schema),
|
||||
this.editorService.getLoader(action.payload).getConfigs(),
|
||||
this.editorService.getLoader(action.payload).getRelease().pipe(
|
||||
this.editorService.configLoader.getConfigs(),
|
||||
this.editorService.configLoader.getRelease().pipe(
|
||||
map(result => [result.deploymentHistory, result.storedDeployment])
|
||||
),
|
||||
]))
|
||||
),
|
||||
this.editorService.getUser(),
|
||||
this.editorService.getLoader(action.payload).getPullRequestStatus(),
|
||||
this.editorService.configLoader.getPullRequestStatus(),
|
||||
this.editorService.getSensorFields(),
|
||||
this.editorService.getTestCaseSchema(),
|
||||
this.editorService.getLoader(action.payload).getTestSpecificationSchema(),
|
||||
this.editorService.configLoader.getTestSpecificationSchema(),
|
||||
]).pipe(
|
||||
map(([[configSchema, configs, [deploymentHistory, storedDeployment]],
|
||||
currentUser, pullRequestPending, sensorFields, testCaseSchema, testSpecificationSchema]: any) => {
|
||||
@@ -61,9 +63,9 @@ export class EditorEffects {
|
||||
loadRepositories$: Observable<Action> = this.actions$.pipe(
|
||||
ofType<actions.LoadRepositories>(actions.LOAD_REPOSITORIES),
|
||||
withLatestFrom(this.store.select(fromStore.getServiceNames)),
|
||||
exhaustMap(([action, serviceNames]) => {
|
||||
exhaustMap(([_, serviceNames]) => {
|
||||
return forkJoin(serviceNames.
|
||||
map(serviceName => this.editorService.getLoader(serviceName).getRepositoryLinks())
|
||||
map(serviceName => this.editorService.getRepositoryLinks(serviceName))
|
||||
).pipe(
|
||||
map((result: RepositoryLinks[]) => new actions.LoadRepositoriesSuccess(result)),
|
||||
catchError(error => this.errorHandler(
|
||||
@@ -76,9 +78,8 @@ export class EditorEffects {
|
||||
@Effect()
|
||||
loadPullRequestStatus$ = this.actions$.pipe(
|
||||
ofType<actions.LoadPullRequestStatus>(actions.LOAD_PULL_REQUEST_STATUS),
|
||||
withLatestFrom(this.store.select(fromStore.getServiceName)),
|
||||
switchMap(([action, serviceName]) =>
|
||||
this.editorService.getLoader(serviceName).getPullRequestStatus().pipe(
|
||||
switchMap(() =>
|
||||
this.editorService.configLoader.getPullRequestStatus().pipe(
|
||||
map((result) => new actions.LoadPullRequestStatusSuccess(result)),
|
||||
catchError(error => this.errorHandler(
|
||||
error, this.RELEASE_STATUS_FAILED_MESSAGE, of(new actions.LoadPullRequestStatusFailure(error)))
|
||||
@@ -90,9 +91,8 @@ export class EditorEffects {
|
||||
@Effect()
|
||||
loadTestCases$ = this.actions$.pipe(
|
||||
ofType<actions.LoadTestCases>(actions.LOAD_TEST_CASES),
|
||||
withLatestFrom(this.store.select(fromStore.getServiceName)),
|
||||
switchMap(([action, serviceName]) =>
|
||||
this.editorService.getLoader(serviceName).getTestCases().pipe(
|
||||
switchMap(() =>
|
||||
this.editorService.configLoader.getTestCases().pipe(
|
||||
map((result) => new actions.LoadTestCasesSuccess(result)),
|
||||
catchError(error => this.errorHandler(
|
||||
error, this.RELEASE_STATUS_FAILED_MESSAGE, of(new actions.LoadTestCasesFailure(error))))
|
||||
@@ -113,9 +113,8 @@ export class EditorEffects {
|
||||
@Effect()
|
||||
submitRelease$ = this.actions$.pipe(
|
||||
ofType<actions.SubmitRelease>(actions.SUBMIT_RELEASE),
|
||||
withLatestFrom(this.store.select(fromStore.getServiceName)),
|
||||
switchMap(([action, serviceName]) =>
|
||||
this.editorService.getLoader(serviceName).submitRelease(action.payload).pipe(
|
||||
switchMap(action =>
|
||||
this.editorService.configLoader.submitRelease(action.payload).pipe(
|
||||
map(result => {
|
||||
this.displayNotification(this.newSuccessMessage('release'));
|
||||
this.store.dispatch(new actions.LoadPullRequestStatus());
|
||||
@@ -131,14 +130,13 @@ export class EditorEffects {
|
||||
@Effect()
|
||||
submitNewConfig$ = this.actions$.pipe(
|
||||
ofType<actions.SubmitNewConfig>(actions.SUBMIT_NEW_CONFIG),
|
||||
withLatestFrom(this.store.select(fromStore.getServiceName)),
|
||||
switchMap(([action, serviceName]) =>
|
||||
this.editorService.getLoader(serviceName).submitNewConfig(action.payload).pipe(
|
||||
switchMap(action =>
|
||||
this.editorService.configLoader.submitNewConfig(action.payload).pipe(
|
||||
map(result => {
|
||||
this.displayNotification(this.newSuccessMessage('config'));
|
||||
|
||||
return new actions.SubmitNewConfigSuccess(
|
||||
this.editorService.getLoader(serviceName).getConfigsFromFiles(result.attributes.files)
|
||||
this.editorService.configLoader.getConfigsFromFiles(result.attributes.files)
|
||||
);
|
||||
}),
|
||||
catchError(error => this.errorHandler(
|
||||
@@ -149,9 +147,8 @@ export class EditorEffects {
|
||||
@Effect()
|
||||
validateConfig$ = this.actions$.pipe(
|
||||
ofType<actions.ValidateConfig>(actions.VALIDATE_CONFIG),
|
||||
withLatestFrom(this.store.select(fromStore.getServiceName)),
|
||||
switchMap(([action, serviceName]) =>
|
||||
this.editorService.getLoader(serviceName).validateConfig(action.payload).pipe(
|
||||
switchMap(action =>
|
||||
this.editorService.configLoader.validateConfig(action.payload).pipe(
|
||||
map(result => new actions.ValidateConfigsSuccess(result)),
|
||||
catchError(error => this.errorHandler(
|
||||
error, this.VALIDATION_FAILED_MESSAGE, of(new actions.ValidateConfigsFailure(error))))
|
||||
@@ -162,9 +159,8 @@ export class EditorEffects {
|
||||
@Effect()
|
||||
validateConfigs$ = this.actions$.pipe(
|
||||
ofType<actions.ValidateConfigs>(actions.VALIDATE_CONFIGS),
|
||||
withLatestFrom(this.store.select(fromStore.getStoredDeployment), this.store.select(fromStore.getServiceName)),
|
||||
switchMap(([action, deployment, serviceName]) =>
|
||||
this.editorService.getLoader(serviceName).validateRelease(action.payload).pipe(
|
||||
switchMap(action =>
|
||||
this.editorService.configLoader.validateRelease(action.payload).pipe(
|
||||
map(result => new actions.ValidateConfigsSuccess(result)),
|
||||
catchError(error => this.errorHandler(
|
||||
error, this.validationFaliedMessage('config'), of(new actions.ValidateConfigsFailure(error)))))
|
||||
@@ -185,14 +181,13 @@ export class EditorEffects {
|
||||
@Effect()
|
||||
submitRuleEdit$ = this.actions$.pipe(
|
||||
ofType<actions.SubmitConfigEdit>(actions.SUBMIT_CONFIG_EDIT),
|
||||
withLatestFrom(this.store.select(fromStore.getServiceName)),
|
||||
switchMap(([action, serviceName]) =>
|
||||
this.editorService.getLoader(serviceName).submitConfigEdit(action.payload).pipe(
|
||||
tap(action =>
|
||||
this.editorService.configLoader.submitConfigEdit(action.payload).pipe(
|
||||
map(result => {
|
||||
this.displayNotification(this.editSuccessMessage('config'));
|
||||
|
||||
return new actions.SubmitConfigEditSuccess(
|
||||
this.editorService.getLoader(serviceName).getConfigsFromFiles(result.attributes.files)
|
||||
this.editorService.configLoader.getConfigsFromFiles(result.attributes.files)
|
||||
)
|
||||
}),
|
||||
catchError(error => this.errorHandler(
|
||||
@@ -203,9 +198,8 @@ export class EditorEffects {
|
||||
@Effect()
|
||||
submitNewTestCase$ = this.actions$.pipe(
|
||||
ofType<actions.SubmitNewTestCase>(actions.SUBMIT_NEW_TESTCASE),
|
||||
withLatestFrom(this.store.select(fromStore.getServiceName)),
|
||||
switchMap(([action, serviceName]) =>
|
||||
this.editorService.getLoader(serviceName).submitNewTestCase(action.payload).pipe(
|
||||
tap(action =>
|
||||
this.editorService.configLoader.submitNewTestCase(action.payload).pipe(
|
||||
map(m => new actions.SubmitNewTestCaseSuccess(m)),
|
||||
tap(_ => this.displayNotification(this.newSuccessMessage('testcase'))),
|
||||
catchError(error =>
|
||||
@@ -220,9 +214,8 @@ export class EditorEffects {
|
||||
@Effect()
|
||||
submitTestCaseEdit$ = this.actions$.pipe(
|
||||
ofType<actions.SubmitTestCaseEdit>(actions.SUBMIT_TESTCASE_EDIT),
|
||||
withLatestFrom(this.store.select(fromStore.getServiceName)),
|
||||
switchMap(([action, serviceName]) =>
|
||||
this.editorService.getLoader(serviceName).submitTestCaseEdit(action.payload).pipe(
|
||||
tap(action =>
|
||||
this.editorService.configLoader.submitTestCaseEdit(action.payload).pipe(
|
||||
map(m => new actions.SubmitTestCaseEditSuccess(m)),
|
||||
tap(_ => this.displayNotification(this.editSuccessMessage('testcase'))),
|
||||
catchError(error => this.errorHandler(
|
||||
|
||||
@@ -2,9 +2,9 @@ import { StatusCode } from '@app/commons';
|
||||
import {
|
||||
ConfigData, ConfigWrapper, Deployment, EditorResult, ExceptionInfo,
|
||||
FileHistory, PullRequestInfo, RepositoryLinks, SchemaDto, SensorFields, SubmitStatus,
|
||||
} from '@app/model';
|
||||
} from '@model';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { TestCaseMap } from '../model/test-case';
|
||||
import { TestCaseMap } from '@model/test-case';
|
||||
import * as editor from './editor.actions';
|
||||
|
||||
export interface State {
|
||||
@@ -39,6 +39,7 @@ export interface State {
|
||||
testCaseSchema: any;
|
||||
testCaseMap: TestCaseMap;
|
||||
testSpecificationSchema: any;
|
||||
modelOrder: object;
|
||||
}
|
||||
|
||||
export const initialState: State = {
|
||||
@@ -73,6 +74,7 @@ export const initialState: State = {
|
||||
testCaseSchema: {},
|
||||
testCaseMap: undefined,
|
||||
testSpecificationSchema: undefined,
|
||||
modelOrder: {},
|
||||
}
|
||||
|
||||
export function reducer(state = initialState, action: editor.Actions): State {
|
||||
@@ -313,6 +315,11 @@ export function reducer(state = initialState, action: editor.Actions): State {
|
||||
testCaseMap: testCaseMap2,
|
||||
});
|
||||
|
||||
case editor.SET_MODEL_ORDER:
|
||||
return Object.assign({}, state, {
|
||||
modelOrder: action.payload,
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
},
|
||||
"exclude": [
|
||||
"test.ts",
|
||||
"**/*.spec.ts"
|
||||
"**/*.spec.ts",
|
||||
"src/testing/*",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -23,7 +23,10 @@
|
||||
],
|
||||
"paths": {
|
||||
"@app/*": ["app/*"],
|
||||
"@env/*": ["environments/*"]
|
||||
"@env/*": ["environments/*"],
|
||||
"@model/*": ["app/model/*"],
|
||||
"@model": ["app/model/index"],
|
||||
"@services/*": ["app/services/*"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user