Create typed policy editor component 41/5741/2
authorelinuxhenrik <henrik.b.andersson@est.tech>
Tue, 9 Mar 2021 08:23:01 +0000 (09:23 +0100)
committerelinuxhenrik <henrik.b.andersson@est.tech>
Tue, 9 Mar 2021 10:12:08 +0000 (11:12 +0100)
Change-Id: I544d025b57fdbea823424033ee5a48ebb0655916
Signed-off-by: elinuxhenrik <henrik.b.andersson@est.tech>
Issue-ID: NONRTRIC-462

webapp-frontend/src/app/policy/policy-instance-dialog/policy-instance-dialog.component.html
webapp-frontend/src/app/policy/policy-instance-dialog/policy-instance-dialog.component.ts
webapp-frontend/src/app/policy/policy.module.ts
webapp-frontend/src/app/policy/typed-policy-editor/typed-policy-editor.component.html [new file with mode: 0644]
webapp-frontend/src/app/policy/typed-policy-editor/typed-policy-editor.component.scss [new file with mode: 0644]
webapp-frontend/src/app/policy/typed-policy-editor/typed-policy-editor.component.spec.ts [new file with mode: 0644]
webapp-frontend/src/app/policy/typed-policy-editor/typed-policy-editor.component.ts [new file with mode: 0644]

index 2d0526e..695dc6d 100644 (file)
@@ -19,7 +19,7 @@
   -->
 
 <div class="text-muted logo" fxLayout="row" fxLayoutGap="50px" fxLayoutAlign="space-around center">
-    <div *ngIf="policyInstanceId">[{{this.ric}}] Instance ID: {{policyInstanceId}}</div>
+    <div *ngIf="policyInstanceId">[{{ric}}] Instance ID: {{policyInstanceId}}</div>
 </div>
 <div class="mat-elevation-z8 header row" [ngClass]="{'header-dark': darkMode}">
     <div class="logo">
@@ -27,9 +27,9 @@
         <svg class="logo__icon" viewBox="150.3 22.2 1000 50">
             <text class="logo__text" [ngClass]="{'logo__text-dark': darkMode}" font-size="30" font-weight="600"
                 letter-spacing=".1em" transform="translate(149 56)">
-                <tspan *ngIf="!this.policyInstanceId">Create new policy instance of type </tspan>
-                <tspan *ngIf="jsonSchemaObject.title"> {{this.jsonSchemaObject.title}}</tspan>
-                <tspan *ngIf="!jsonSchemaObject.title"> {{this.policyTypeName}}</tspan>
+                <tspan *ngIf="!policyInstanceId">Create new policy instance of type </tspan>
+                <tspan *ngIf="jsonSchemaObject.title"> {{jsonSchemaObject.title}}</tspan>
+                <tspan *ngIf="!jsonSchemaObject.title"> {{policyTypeName}}</tspan>
             </text>
         </svg>
     </div>
     fxLayoutAlign.lt-sm="flex-start center">
 
     <mat-card class="card" [ngClass]="{'card-dark': darkMode}">
-        <mat-form-field *ngIf="!this.policyInstanceId" appearance="fill">
-            <mat-select id="ricSelector" formControlName="ricSelector" matInput required [(value)]="this.ric"
+        <mat-form-field *ngIf="!policyInstanceId" appearance="fill">
+            <mat-select id="ricSelector" formControlName="ricSelector" matInput required [(value)]="ric"
                 placeholder="Target"
                 matTooltip="Element where the policy instance resides, e.g. a gNodeB or Near-RT RIC">
-                <mat-option *ngFor="let ric of this.allRics" [value]="ric">
+                <mat-option *ngFor="let ric of allRics" [value]="ric">
                     {{ric.ric_id}}
                 </mat-option>
             </mat-select>
             </div>
         </mat-form-field>
 
-        <h4 class="default-cursor" (click)="toggleVisible('form')">
-            <mat-icon matTooltip="Properties">{{isVisible.form ? 'expand_less' : 'expand_more'}}</mat-icon>
-            Properties
-        </h4>
-        <div *ngIf="isVisible.form" class="json-schema-form" [@expandSection]="true">
-            <div *ngIf="!formActive">{{jsonFormStatusMessage}}</div>
-
-            <json-schema-form *ngIf="formActive" [form]="jsonSchemaObject"
-                [(data)]="jsonObject" [options]="jsonFormOptions" [framework]="'material-design'" [language]="'en'"
-                (onChanges)="onChanges($event)" (onSubmit)="onSubmit()" (isValid)="isValid($event)"
-                (validationErrors)="validationErrors($event)">
-            </json-schema-form>
-        </div>
+        <nrcp-typed-policy-editor [jsonSchemaObject]="jsonSchemaObject" [jsonObject]="data.instanceJson" [darkMode]="darkMode"></nrcp-typed-policy-editor>
         <hr />
-        <button mat-raised-button (click)="this.onClose()">Close</button>
-        <button mat-raised-button (click)="this.onSubmit()" [disabled]="!this.formIsValid || !this.ric"
+        <button mat-raised-button (click)="onClose()">Close</button>
+        <button mat-raised-button (click)="onSubmit()" [disabled]="!isJsonFormValid || !ric"
             class="submitBtn">Submit</button>
         <hr />
-        <h4 [class.text-danger]="!formIsValid && !isVisible.json" [class.default-cursor]="formIsValid || isVisible.json"
-            (click)="toggleVisible('json')">
-            <mat-icon matTooltip="Json">{{isVisible.json ? 'expand_less' : 'expand_more'}}</mat-icon>
-            Json
-        </h4>
-        <div *ngIf="isVisible.json" fxLayout="column" [@expandSection]="true">
-            <div>
-                <strong *ngIf="formIsValid || prettyValidationErrors" [class.text-muted]="formIsValid"
-                    [class.text-danger]="!formIsValid">
-                    {{formIsValid ? 'Json' : 'Not valid'}}
-                </strong>
-                <span *ngIf="!formIsValid && !prettyValidationErrors">Invalid form</span>
-                <span *ngIf="prettyValidationErrors">— errors:</span>
-                <div *ngIf="prettyValidationErrors" class="text-danger" [innerHTML]="prettyValidationErrors"></div>
-            </div>
-            <div>
-                <pre [class.text__dark]="this.darkMode">{{prettyLiveFormData}}</pre>
-            </div>
-        </div>
-
-        <h4 class="default-cursor" (click)="toggleVisible('schema')">
-            <mat-icon matTooltip="Json Schema">{{isVisible.schema ? 'expand_less' : 'expand_more'}}</mat-icon>
-            Json Schema
-        </h4>
-        <div *ngIf="isVisible.schema" fxLayout="column" [@expandSection]="true">
-            <strong class="text-muted">Schema</strong>
-            <pre [class.text__dark]="this.darkMode">{{schemaAsString}}</pre>
-        </div>
     </mat-card>
 </div>
\ No newline at end of file
index a2bd7a5..3c2ea2b 100644 (file)
  * limitations under the License.
  * ========================LICENSE_END===================================
  */
-import { animate, state, style, transition, trigger } from '@angular/animations';
 import { AfterViewInit, Component, Inject, OnInit, ViewChild } from '@angular/core';
 import { MatDialogConfig, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
-import { JsonPointer } from 'angular6-json-schema-form';
 import * as uuid from 'uuid';
 import { CreatePolicyInstance, PolicyInstance, PolicyTypeSchema } from '../../interfaces/policy.types';
 import { PolicyService } from '../../services/policy/policy.service';
@@ -31,59 +29,26 @@ import { HttpErrorResponse } from '@angular/common/http';
 import { FormGroup, FormControl, Validators } from '@angular/forms';
 import { ChangeDetectorRef } from '@angular/core';
 import { Ric, Rics } from '../../interfaces/ric';
+import { TypedPolicyEditorComponent } from '../typed-policy-editor/typed-policy-editor.component';
 
 
 @Component({
     selector: 'nrcp-policy-instance-dialog',
     templateUrl: './policy-instance-dialog.component.html',
-    styleUrls: ['./policy-instance-dialog.component.scss'],
-    animations: [
-        trigger('expandSection', [
-            state('in', style({ height: '*' })),
-            transition(':enter', [
-                style({ height: 0 }), animate(100),
-            ]),
-            transition(':leave', [
-                style({ height: '*' }),
-                animate(100, style({ height: 0 })),
-            ]),
-        ]),
-    ],
+    styleUrls: ['./policy-instance-dialog.component.scss']
 })
 export class PolicyInstanceDialogComponent implements OnInit, AfterViewInit {
+    @ViewChild(TypedPolicyEditorComponent)
+    policyEditor: TypedPolicyEditorComponent;
     instanceForm: FormGroup;
 
 
-    formActive = false;
-    isVisible = {
-        form: true,
-        json: false,
-        schema: false
-    };
-
-    jsonFormStatusMessage = 'Loading form...';
-    jsonSchemaObject: any = {};
-    jsonObject: any = {};
-
-
-    jsonFormOptions: any = {
-        addSubmit: false, // Add a submit button if layout does not have one
-        debug: false, // Don't show inline debugging information
-        loadExternalAssets: true, // Load external css and JavaScript for frameworks
-        returnEmptyFields: false, // Don't return values for empty input fields
-        setSchemaDefaults: true, // Always use schema defaults for empty fields
-        defautWidgetOptions: { feedback: true }, // Show inline feedback icons
-    };
-
-    liveFormData: any = {};
-    formValidationErrors: any;
-    formIsValid = false;
-
+    ric: string;
+    allRics: Ric[];
     policyInstanceId: string; // null if not yet created
     policyTypeName: string;
+    jsonSchemaObject: any = {};
     darkMode: boolean;
-    ric: string;
-    allRics: Ric[];
 
     private fetchRics() {
         console.log('fetchRics ' + this.policyTypeName);
@@ -102,20 +67,16 @@ export class PolicyInstanceDialogComponent implements OnInit, AfterViewInit {
         private dataService: PolicyService,
         private errorService: ErrorDialogService,
         private notificationService: NotificationService,
-        @Inject(MAT_DIALOG_DATA) private data,
+        @Inject(MAT_DIALOG_DATA) public data,
         private dialogRef: MatDialogRef<PolicyInstanceDialogComponent>,
         private ui: UiService) {
-        this.formActive = false;
         this.policyInstanceId = data.instanceId;
         this.policyTypeName = data.name;
         this.jsonSchemaObject = data.createSchema;
-        this.jsonObject = data.instanceJson;
         this.ric = data.ric;
     }
 
     ngOnInit() {
-        this.jsonFormStatusMessage = 'Init';
-        this.formActive = true;
         this.ui.darkModeState.subscribe((isDark) => {
             this.darkMode = isDark;
         });
@@ -139,7 +100,7 @@ export class PolicyInstanceDialogComponent implements OnInit, AfterViewInit {
         if (this.policyInstanceId == null) {
             this.policyInstanceId = uuid.v4();
         }
-        const policyJson: string = this.prettyLiveFormData;
+        const policyJson: string = this.policyEditor.prettyLiveFormData;
         const self: PolicyInstanceDialogComponent = this;
         let createPolicyInstance: CreatePolicyInstance = this.createPolicyInstance(policyJson);
         this.dataService.putPolicy(createPolicyInstance).subscribe(
@@ -170,65 +131,8 @@ export class PolicyInstanceDialogComponent implements OnInit, AfterViewInit {
         this.dialogRef.close();
     }
 
-    public onChanges(formData: any) {
-        this.liveFormData = formData;
-    }
-
-    get prettyLiveFormData(): string {
-        return JSON.stringify(this.liveFormData, null, 2);
-    }
-
-    get schemaAsString(): string {
-        return JSON.stringify(this.jsonSchemaObject, null, 2);
-    }
-
-    get jsonAsString(): string {
-        return JSON.stringify(this.jsonObject, null, 2);
-    }
-
-    isValid(isValid: boolean): void {
-        this.formIsValid = isValid;
-    }
-
-    validationErrors(validationErrors: any): void {
-        this.formValidationErrors = validationErrors;
-    }
-
-    get prettyValidationErrors() {
-        if (!this.formValidationErrors) { return null; }
-        const errorArray = [];
-        for (const error of this.formValidationErrors) {
-            const message = error.message;
-            const dataPathArray = JsonPointer.parse(error.dataPath);
-            if (dataPathArray.length) {
-                let field = dataPathArray[0];
-                for (let i = 1; i < dataPathArray.length; i++) {
-                    const key = dataPathArray[i];
-                    field += /^\d+$/.test(key) ? `[${key}]` : `.${key}`;
-                }
-                errorArray.push(`${field}: ${message}`);
-            } else {
-                errorArray.push(message);
-            }
-        }
-        return errorArray.join('<br>');
-    }
-
-    private parseJson(str: string): string {
-        try {
-            if (str != null) {
-                return JSON.parse(str);
-            }
-        } catch (jsonError) {
-            this.jsonFormStatusMessage =
-                'Invalid JSON\n' +
-                'parser returned:\n\n' + jsonError;
-        }
-        return null;
-    }
-
-    public toggleVisible(item: string) {
-        this.isVisible[item] = !this.isVisible[item];
+    get isJsonFormValid(): boolean {
+        return this.policyEditor ? this.policyEditor.formIsValid : false;
     }
 }
 
index 9bf2959..5f7e4fc 100644 (file)
@@ -1,3 +1,23 @@
+// -
+//   ========================LICENSE_START=================================
+//   O-RAN-SC
+//   %%
+//   Copyright (C) 2021: Nordix Foundation
+//   %%
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
 import { CommonModule } from '@angular/common';
 import { NgModule } from '@angular/core';
 import { MatTableModule } from '@angular/material/table';
@@ -33,6 +53,7 @@ import { FlexLayoutModule } from '@angular/flex-layout';
 import { Routes, RouterModule } from '@angular/router';
 import { RicSelectorComponent } from './ric-selector/ric-selector.component';
 import { NoTypePolicyEditorComponent } from './no-type-policy-editor/no-type-policy-editor.component';
+import { TypedPolicyEditorComponent } from './typed-policy-editor/typed-policy-editor.component';
 
 const routes:Routes = [
   {path: 'policy', component: PolicyControlComponent}
@@ -47,6 +68,7 @@ const routes:Routes = [
     PolicyInstanceDialogComponent,
     RicSelectorComponent,
     NoTypePolicyEditorComponent,
+    TypedPolicyEditorComponent,
   ],
   imports: [
     CommonModule,
diff --git a/webapp-frontend/src/app/policy/typed-policy-editor/typed-policy-editor.component.html b/webapp-frontend/src/app/policy/typed-policy-editor/typed-policy-editor.component.html
new file mode 100644 (file)
index 0000000..9e6e1e4
--- /dev/null
@@ -0,0 +1,63 @@
+<!--
+ -
+   ========================LICENSE_START=================================
+   O-RAN-SC
+   %%
+   Copyright (C) 2021: Nordix Foundation
+   %%
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+   ========================LICENSE_END===================================
+
+-->
+
+<h4 class="default-cursor" (click)="toggleVisible('form')">
+    <mat-icon matTooltip="Properties">{{isVisible.form ? 'expand_less' : 'expand_more'}}</mat-icon>
+    Properties
+</h4>
+<div *ngIf="isVisible.form" class="json-schema-form" [@expandSection]="true">
+    <div *ngIf="!formActive">{{jsonFormStatusMessage}}</div>
+
+    <json-schema-form id="json-schema-form" *ngIf="formActive" [form]="jsonSchemaObject"
+        [(data)]="jsonObject" [options]="jsonFormOptions" [framework]="'material-design'" [language]="'en'"
+        (onChanges)="onChanges($event)" (isValid)="isValid($event)"
+        (validationErrors)="validationErrors($event)">
+    </json-schema-form>
+</div>
+<h4 [class.text-danger]="!formIsValid && !isVisible.json" [class.default-cursor]="formIsValid || isVisible.json"
+(click)="toggleVisible('json')">
+<mat-icon matTooltip="Json">{{isVisible.json ? 'expand_less' : 'expand_more'}}</mat-icon>
+Json
+</h4>
+<div *ngIf="isVisible.json" fxLayout="column" [@expandSection]="true">
+<div>
+    <strong *ngIf="formIsValid || prettyValidationErrors" [class.text-muted]="formIsValid"
+        [class.text-danger]="!formIsValid">
+        {{formIsValid ? 'Json' : 'Not valid'}}
+    </strong>
+    <span *ngIf="!formIsValid && !prettyValidationErrors">Invalid form</span>
+    <span *ngIf="prettyValidationErrors">— errors:</span>
+    <div *ngIf="prettyValidationErrors" class="text-danger" [innerHTML]="prettyValidationErrors"></div>
+</div>
+<div>
+    <pre [class.text__dark]="darkMode">{{prettyLiveFormData}}</pre>
+</div>
+</div>
+
+<h4 class="default-cursor" (click)="toggleVisible('schema')">
+<mat-icon matTooltip="Json Schema">{{isVisible.schema ? 'expand_less' : 'expand_more'}}</mat-icon>
+Json Schema
+</h4>
+<div *ngIf="isVisible.schema" fxLayout="column" [@expandSection]="true">
+<strong class="text-muted">Schema</strong>
+<pre [class.text__dark]="darkMode">{{schemaAsString}}</pre>
+</div>
diff --git a/webapp-frontend/src/app/policy/typed-policy-editor/typed-policy-editor.component.scss b/webapp-frontend/src/app/policy/typed-policy-editor/typed-policy-editor.component.scss
new file mode 100644 (file)
index 0000000..3666abc
--- /dev/null
@@ -0,0 +1,23 @@
+// -
+//   ========================LICENSE_START=================================
+//   O-RAN-SC
+//   %%
+//   Copyright (C) 2021: Nordix Foundation
+//   %%
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+.text__dark {
+    color: white;
+}
diff --git a/webapp-frontend/src/app/policy/typed-policy-editor/typed-policy-editor.component.spec.ts b/webapp-frontend/src/app/policy/typed-policy-editor/typed-policy-editor.component.spec.ts
new file mode 100644 (file)
index 0000000..9f58af2
--- /dev/null
@@ -0,0 +1,41 @@
+import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/compiler';
+import { ChangeDetectorRef } from '@angular/core';
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { MatIconModule } from '@angular/material/icon';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+
+import { TypedPolicyEditorComponent } from './typed-policy-editor.component';
+
+describe('TypedPolicyEditorComponent', () => {
+  let component: TypedPolicyEditorComponent;
+  let fixture: ComponentFixture<TypedPolicyEditorComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      imports: [
+        BrowserAnimationsModule,
+        MatIconModule
+      ],
+      declarations: [
+        TypedPolicyEditorComponent
+      ],
+      schemas: [
+        CUSTOM_ELEMENTS_SCHEMA
+      ],
+      providers: [
+        ChangeDetectorRef
+      ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(TypedPolicyEditorComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/webapp-frontend/src/app/policy/typed-policy-editor/typed-policy-editor.component.ts b/webapp-frontend/src/app/policy/typed-policy-editor/typed-policy-editor.component.ts
new file mode 100644 (file)
index 0000000..d05c78f
--- /dev/null
@@ -0,0 +1,128 @@
+// -
+//   ========================LICENSE_START=================================
+//   O-RAN-SC
+//   %%
+//   Copyright (C) 2021: Nordix Foundation
+//   %%
+//   Licensed under the Apache License, Version 2.0 (the "License");
+//   you may not use this file except in compliance with the License.
+//   You may obtain a copy of the License at
+//
+//        http://www.apache.org/licenses/LICENSE-2.0
+//
+//   Unless required by applicable law or agreed to in writing, software
+//   distributed under the License is distributed on an "AS IS" BASIS,
+//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//   See the License for the specific language governing permissions and
+//   limitations under the License.
+//   ========================LICENSE_END===================================
+//
+
+import { animate, state, style, transition, trigger } from '@angular/animations';
+import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
+import { JsonPointer } from 'angular6-json-schema-form';
+
+@Component({
+    selector: 'nrcp-typed-policy-editor',
+    templateUrl: './typed-policy-editor.component.html',
+    styleUrls: ['./typed-policy-editor.component.scss'],
+    animations: [
+        trigger('expandSection', [
+            state('in', style({ height: '*' })),
+            transition(':enter', [
+                style({ height: 0 }), animate(100),
+            ]),
+            transition(':leave', [
+                style({ height: '*' }),
+                animate(100, style({ height: 0 })),
+            ]),
+        ]),
+    ],
+
+})
+export class TypedPolicyEditorComponent implements OnInit {
+    jsonFormOptions: any = {
+        addSubmit: false, // Add a submit button if layout does not have one
+        debug: false, // Don't show inline debugging information
+        loadExternalAssets: false, // Load external css and JavaScript for frameworks
+        returnEmptyFields: false, // Don't return values for empty input fields
+        setSchemaDefaults: true, // Always use schema defaults for empty fields
+        defautWidgetOptions: { feedback: true }, // Show inline feedback icons
+    };
+
+    @Input() jsonSchemaObject: any = {};
+    @Input() jsonObject: any = {};
+    @Input() darkMode: boolean;
+
+    isVisible = {
+        form: true,
+        json: false,
+        schema: false
+    };
+    formActive: boolean = false;
+    jsonFormStatusMessage: string = 'Loading form...';
+    liveFormData: any = {};
+    formIsValid: boolean = false;
+    formValidationErrors: any;
+
+    constructor(
+        private cdr: ChangeDetectorRef) {
+        this.formActive = false;
+    }
+
+    ngOnInit(): void {
+         this.formActive = true;
+    }
+
+    ngAfterViewInit() {
+        this.cdr.detectChanges();
+    }
+
+    public onChanges(formData: any) {
+        this.liveFormData = formData;
+    }
+
+    get prettyLiveFormData(): string {
+        return JSON.stringify(this.liveFormData, null, 2);
+    }
+
+    get schemaAsString(): string {
+        return JSON.stringify(this.jsonSchemaObject, null, 2);
+    }
+
+    get jsonAsString(): string {
+        return JSON.stringify(this.jsonObject, null, 2);
+    }
+
+    isValid(isValid: boolean): void {
+        this.formIsValid = isValid;
+    }
+
+    validationErrors(validationErrors: any): void {
+        this.formValidationErrors = validationErrors;
+    }
+
+    get prettyValidationErrors() {
+        if (!this.formValidationErrors) { return null; }
+        const errorArray = [];
+        for (const error of this.formValidationErrors) {
+            const message = error.message;
+            const dataPathArray = JsonPointer.parse(error.dataPath);
+            if (dataPathArray.length) {
+                let field = dataPathArray[0];
+                for (let i = 1; i < dataPathArray.length; i++) {
+                    const key = dataPathArray[i];
+                    field += /^\d+$/.test(key) ? `[${key}]` : `.${key}`;
+                }
+                errorArray.push(`${field}: ${message}`);
+            } else {
+                errorArray.push(message);
+            }
+        }
+        return errorArray.join('<br>');
+    }
+
+    public toggleVisible(item: string) {
+        this.isVisible[item] = !this.isVisible[item];
+    }
+}