Non-RT RIC Dashboard
First commit
Change-Id: I9e140d31d65d13df3ce07f6b87eac250ee952eab
Issue-ID: NONRTRIC-61
Signed-off-by: PatrikBuhr <patrik.buhr@est.tech>
diff --git a/dashboard/webapp-frontend/src/app/policy-control/policy-control.component.html b/dashboard/webapp-frontend/src/app/policy-control/policy-control.component.html
new file mode 100644
index 0000000..04d440c
--- /dev/null
+++ b/dashboard/webapp-frontend/src/app/policy-control/policy-control.component.html
@@ -0,0 +1,82 @@
+<!--
+ ========================LICENSE_START=================================
+ O-RAN-SC
+ %%
+ Copyright (C) 2019 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===================================
+ -->
+
+<div>
+ <h3 class="rd-global-page-title">Policy Control</h3>
+
+ <table mat-table [dataSource]="policyTypesDataSource" matSort multiTemplateDataRows
+ class="policy-type-table mat-elevation-z8">
+
+ <ng-container matColumnDef="name">
+ <mat-header-cell *matHeaderCellDef mat-sort-header>Policy Type</mat-header-cell>
+ <mat-cell *matCellDef="let policyType">
+ <mat-icon matTooltip="Properties">{{isInstancesShown(policyType) ? 'expand_less' : 'expand_more'}}</mat-icon>
+ {{getPolicyTypeName(policyType)}}
+ </mat-cell>
+ </ng-container>
+
+ <ng-container matColumnDef="description">
+ <mat-header-cell *matHeaderCellDef> Description </mat-header-cell>
+ <mat-cell *matCellDef="let policyType"> {{policyType.description}} </mat-cell>
+ </ng-container>
+
+ <ng-container matColumnDef="action">
+ <mat-header-cell class="action-cell" *matHeaderCellDef>Action </mat-header-cell>
+ <mat-cell class="action-cell" *matCellDef="let policyType" (click)="$event.stopPropagation()">
+ <button mat-icon-button (click)="createPolicyInstance(policyType)">
+ <mat-icon matTooltip="Create instance">add_box</mat-icon>
+ </button>
+ </mat-cell>
+ </ng-container>
+
+ <!-- =================== Policy instances for one type ======================== -->
+ <ng-container matColumnDef="instanceTableContainer">
+ <mat-cell *matCellDef="let policyType">
+ <rd-policy-instance
+ [policyType]=policyType
+ [expanded]=getObservable(policyType)>
+ </rd-policy-instance>
+ </mat-cell>
+ </ng-container>
+ <!-- ======= -->
+
+ <ng-container matColumnDef="noRecordsFound">
+ <mat-footer-cell *matFooterCellDef>No records found.</mat-footer-cell>
+ </ng-container>
+
+ <mat-header-row *matHeaderRowDef="['name', 'description', 'action']"></mat-header-row>
+ <mat-row *matRowDef="let policyType; columns: ['name', 'description', 'action']"
+ (click)="toggleListInstances(policyType)">
+ </mat-row>
+
+ <mat-row *matRowDef="let policyType; columns: ['instanceTableContainer'];"
+ [@detailExpand]="isInstancesShown(policyType) ? 'expanded' : 'collapsed'" style="overflow: hidden">
+ </mat-row>
+
+ <mat-footer-row *matFooterRowDef="['noRecordsFound']"
+ [ngClass]="{'display-none': policyTypesDataSource.rowCount > 0}">
+ </mat-footer-row>
+
+ </table>
+
+ <div class="spinner-container" *ngIf="policyTypesDataSource.loading$ | async">
+ <mat-spinner diameter="50"></mat-spinner>
+ </div>
+</div>
\ No newline at end of file
diff --git a/dashboard/webapp-frontend/src/app/policy-control/policy-control.component.scss b/dashboard/webapp-frontend/src/app/policy-control/policy-control.component.scss
new file mode 100644
index 0000000..f93e4ff
--- /dev/null
+++ b/dashboard/webapp-frontend/src/app/policy-control/policy-control.component.scss
@@ -0,0 +1,45 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 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===================================
+ */
+
+.spinner-container {
+ height: 100px;
+ width: 100px;
+}
+
+.spinner-container mat-spinner {
+ margin: 0 auto 0 auto;
+}
+
+.policy-type-table {
+ width: 100%;
+ min-height: 150px;
+ margin-top: 10px;
+ margin-bottom: 10px;
+ background-color: transparent;
+}
+
+.action-cell {
+ display: flex;
+ justify-content: flex-end;
+}
+
+.display-none {
+ display: none;
+}
diff --git a/dashboard/webapp-frontend/src/app/policy-control/policy-control.component.spec.ts b/dashboard/webapp-frontend/src/app/policy-control/policy-control.component.spec.ts
new file mode 100644
index 0000000..7c8643a
--- /dev/null
+++ b/dashboard/webapp-frontend/src/app/policy-control/policy-control.component.spec.ts
@@ -0,0 +1,44 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 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 { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { PolicyControlComponent } from './policy-control.component';
+
+describe('PolicyControlComponent', () => {
+ let component: PolicyControlComponent;
+ let fixture: ComponentFixture<PolicyControlComponent>;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ PolicyControlComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(PolicyControlComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/dashboard/webapp-frontend/src/app/policy-control/policy-control.component.ts b/dashboard/webapp-frontend/src/app/policy-control/policy-control.component.ts
new file mode 100644
index 0000000..70b8c45
--- /dev/null
+++ b/dashboard/webapp-frontend/src/app/policy-control/policy-control.component.ts
@@ -0,0 +1,115 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 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 { Component, OnInit, ViewChild } from '@angular/core';
+import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
+import { MatSort } from '@angular/material/sort';
+import { animate, state, style, transition, trigger } from '@angular/animations';
+import { HttpErrorResponse, HttpResponse } from '@angular/common/http';
+
+import { PolicyService } from '../services/policy/policy.service';
+import { PolicyType } from '../interfaces/policy.types';
+import { PolicyTypeDataSource } from './policy-type.datasource';
+import { PolicyInstanceDataSource } from './policy-instance.datasource';
+import { getPolicyDialogProperties } from './policy-instance-dialog.component';
+import { PolicyInstanceDialogComponent } from './policy-instance-dialog.component';
+import { PolicyInstance } from '../interfaces/policy.types';
+import { NotificationService } from '../services/ui/notification.service';
+import { ErrorDialogService } from '../services/ui/error-dialog.service';
+import { ConfirmDialogService } from './../services/ui/confirm-dialog.service';
+import { Subject } from 'rxjs';
+
+class PolicyTypeInfo {
+ constructor(public type: PolicyType, public isExpanded: boolean) { }
+
+ isExpandedObservers: Subject<boolean> = new Subject<boolean>();
+};
+
+@Component({
+ selector: 'rd-policy-control',
+ templateUrl: './policy-control.component.html',
+ styleUrls: ['./policy-control.component.scss'],
+ animations: [
+ trigger('detailExpand', [
+ state('collapsed', style({ height: '0px', minHeight: '0', visibility: 'hidden' })),
+ state('expanded', style({ height: '*', visibility: 'visible' })),
+ transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
+ ]),
+ ],
+})
+export class PolicyControlComponent implements OnInit {
+
+ policyTypesDataSource: PolicyTypeDataSource;
+ @ViewChild(MatSort, { static: true }) sort: MatSort;
+
+ expandedTypes = new Map<string, PolicyTypeInfo>();
+
+ constructor(
+ private policySvc: PolicyService,
+ private dialog: MatDialog,
+ private errorDialogService: ErrorDialogService,
+ private notificationService: NotificationService,
+ private confirmDialogService: ConfirmDialogService) { }
+
+ ngOnInit() {
+ this.policyTypesDataSource = new PolicyTypeDataSource(this.policySvc, this.sort, this.notificationService);
+ this.policyTypesDataSource.loadTable();
+ }
+
+ createPolicyInstance(policyType: PolicyType): void {
+ const dialogRef = this.dialog.open(PolicyInstanceDialogComponent, getPolicyDialogProperties(policyType, null));
+ const info: PolicyTypeInfo = this.getPolicyTypeInfo(policyType);
+ dialogRef.afterClosed().subscribe(
+ (result: any) => {
+ info.isExpandedObservers.next(info.isExpanded);
+ }
+ );
+ }
+
+ toggleListInstances(policyType: PolicyType): void {
+ let info = this.getPolicyTypeInfo(policyType);
+ info.isExpanded = !info.isExpanded;
+ info.isExpandedObservers.next(info.isExpanded);
+ }
+
+ getPolicyTypeInfo(policyType: PolicyType): PolicyTypeInfo {
+ let info: PolicyTypeInfo = this.expandedTypes.get(policyType.name);
+ if (!info) {
+ info = new PolicyTypeInfo(policyType, false);
+ this.expandedTypes.set(policyType.name, info);
+ }
+ return info;
+ }
+
+ isInstancesShown(policyType: PolicyType): boolean {
+ return this.getPolicyTypeInfo(policyType).isExpanded;
+ }
+
+ getPolicyTypeName(type: PolicyType): string {
+ const schema = JSON.parse(type.create_schema);
+ if (schema.title) {
+ return schema.title;
+ }
+ return type.name;
+ }
+
+ getObservable(policyType: PolicyType): Subject<boolean> {
+ return this.getPolicyTypeInfo(policyType).isExpandedObservers;
+ }
+}
diff --git a/dashboard/webapp-frontend/src/app/policy-control/policy-instance-dialog.component.html b/dashboard/webapp-frontend/src/app/policy-control/policy-instance-dialog.component.html
new file mode 100644
index 0000000..ad7ea49
--- /dev/null
+++ b/dashboard/webapp-frontend/src/app/policy-control/policy-instance-dialog.component.html
@@ -0,0 +1,90 @@
+<!--
+ ========================LICENSE_START=================================
+ O-RAN-SC
+ %%
+ Copyright (C) 2019 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===================================
+ -->
+<div class="text-muted logo" fxLayout="row" fxLayoutGap="50px">
+ <div *ngIf="policyInstanceId">{{policyInstanceId}}</div>
+</div>
+<div class="mat-elevation-z8 header row logo">
+ <div class="logo">
+ <img src="../../assets/oran-logo.png" width="30px" height="30px" style="position: relative; z-index: 50" />
+ <svg class="logo__icon" viewBox="150.3 22.2 400 50">
+ <text [ngClass]="{'logo__text-dark': darkModeActive}" class="logo__text" fill="#432c85" font-size="30"
+ font-weight="600" letter-spacing=".1em" transform="translate(149 56)">
+ <tspan>Policy editor</tspan>
+ <tspan *ngIf="jsonSchemaObject.title"> {{this.jsonSchemaObject.title}}</tspan>
+ <tspan *ngIf="!jsonSchemaObject.title"> {{this.policyTypeName}}</tspan>
+ </text>
+ </svg>
+ </div>
+</div>
+
+<!--<div class="text-muted" *ngIf="jsonSchemaObject.description">{{jsonSchemaObject.description}}</div>-->
+
+<div fxLayout="row" fxLayoutAlign="space-around start" fxLayout.lt-sm="column" fxLayoutAlign.lt-sm="flex-start center">
+ <mat-card class="card">
+ <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" loadExternalAssets="true" [form]="jsonSchemaObject"
+ [(data)]="jsonObject" [options]="jsonFormOptions" [framework]="'material-design'" [language]="'en'"
+ (onChanges)="onChanges($event)" (onSubmit)="onSubmit($event)" (isValid)="isValid($event)"
+ (validationErrors)="validationErrors($event)">
+ </json-schema-form>
+ </div>
+ <hr />
+ <button mat-raised-button (click)="this.onSubmit()" [disabled]="!this.formIsValid" class="submitBtn"
+ style="margin-right:10px">Submit</button>
+ <button mat-raised-button (click)="this.onClose()">Close</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.data-good]="!prettyValidationErrors && prettyLiveFormData !== '{}'"
+ [class.data-bad]="prettyValidationErrors">{{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>{{schemaAsString}}</pre>
+ </div>
+ </mat-card>
+</div>
\ No newline at end of file
diff --git a/dashboard/webapp-frontend/src/app/policy-control/policy-instance-dialog.component.scss b/dashboard/webapp-frontend/src/app/policy-control/policy-instance-dialog.component.scss
new file mode 100644
index 0000000..7050020
--- /dev/null
+++ b/dashboard/webapp-frontend/src/app/policy-control/policy-instance-dialog.component.scss
@@ -0,0 +1,56 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 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===================================
+ */
+
+.header {
+ color: black;
+ background: linear-gradient(to right, white 0%, rgb(217, 216, 231) 100%);
+ font-size: 40px;
+ font-weight: 400;
+ margin-top: 10px;
+ margin-bottom: 10px;
+}
+
+.logo {
+ margin-left: 10px;
+}
+
+.logo__text {
+ fill: #2B244D;
+}
+
+.logo__text-dark {
+ fill: #ffffff;
+}
+
+.logo__icon {
+ height: 2rem;
+ margin-left: 1rem;
+}
+
+.submitBtn {
+ background-color: #4CAF50; /* Green */
+}
+
+.card {
+ height: 100%;
+ width: 100%;
+ margin-left: 10px;
+ margin-right: 1px;
+}
\ No newline at end of file
diff --git a/dashboard/webapp-frontend/src/app/policy-control/policy-instance-dialog.component.ts b/dashboard/webapp-frontend/src/app/policy-control/policy-instance-dialog.component.ts
new file mode 100644
index 0000000..0f483d4
--- /dev/null
+++ b/dashboard/webapp-frontend/src/app/policy-control/policy-instance-dialog.component.ts
@@ -0,0 +1,214 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 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 { Component, OnInit, ViewChild, Inject, AfterViewInit, Self } from '@angular/core';
+import { MatMenuTrigger } from '@angular/material/menu';
+import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
+import { trigger, state, style, animate, transition } from '@angular/animations';
+import * as uuid from 'uuid';
+
+import { JsonPointer } from 'angular6-json-schema-form';
+import { PolicyService } from '../services/policy/policy.service';
+import { ErrorDialogService } from '../services/ui/error-dialog.service';
+import { NotificationService } from './../services/ui/notification.service';
+
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { PolicyType } from '../interfaces/policy.types';
+import { PolicyInstance } from '../interfaces/policy.types';
+
+@Component({
+ selector: 'rd-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 })),
+ ]),
+ ]),
+ ],
+})
+export class PolicyInstanceDialogComponent implements OnInit, AfterViewInit {
+
+ 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;
+
+
+ @ViewChild(MatMenuTrigger, { static: true }) menuTrigger: MatMenuTrigger;
+
+ public policyInstanceId: string;
+ public policyTypeName: string;
+ private policyTypeId: number;
+
+ constructor(
+ private dataService: PolicyService,
+ private errorService: ErrorDialogService,
+ private notificationService: NotificationService,
+ @Inject(MAT_DIALOG_DATA) private data,
+ private dialogRef: MatDialogRef<PolicyInstanceDialogComponent>) {
+ this.formActive = false;
+ this.policyInstanceId = this.data.instanceId;
+ this.policyTypeName = this.data.name;
+ this.policyTypeId = this.data.policyTypeId;
+ this.parseJson(data.createSchema, data.instanceJson);
+ }
+
+ ngOnInit() {
+ this.jsonFormStatusMessage = 'Init';
+ this.formActive = true;
+ }
+
+ ngAfterViewInit() {
+ }
+
+ onSubmit() {
+ if (this.policyInstanceId == null) {
+ this.policyInstanceId = uuid.v4();
+ }
+ const policyJson: string = this.prettyLiveFormData;
+ const self: PolicyInstanceDialogComponent = this;
+ this.dataService.putPolicy(this.policyTypeId, this.policyInstanceId, policyJson).subscribe(
+ {
+ next(value) {
+ self.notificationService.success('Policy ' + self.policyTypeName + ':' + self.policyInstanceId + ' submitted');
+ },
+ error(error) {
+ self.errorService.displayError('updatePolicy failed: ' + error.message);
+ },
+ complete() { }
+ });
+ }
+
+ onClose() {
+ this.dialogRef.close();
+ }
+
+ public onChanges(data: any) {
+ this.liveFormData = data;
+ }
+
+ get prettyLiveFormData() {
+ return JSON.stringify(this.liveFormData, null, 2);
+ }
+
+ get schemaAsString() {
+ return JSON.stringify(this.jsonSchemaObject, null, 2);
+ }
+
+ get jsonAsString() {
+ return JSON.stringify(this.jsonObject, null, 2);
+ }
+
+ isValid(isValid: boolean): void {
+ this.formIsValid = isValid;
+ }
+
+ validationErrors(data: any): void {
+ this.formValidationErrors = data;
+ }
+
+ 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(createSchema: string, instanceJson: string): void {
+ try {
+ this.jsonSchemaObject = JSON.parse(createSchema);
+ if (this.data.instanceJson != null) {
+ this.jsonObject = JSON.parse(instanceJson);
+ }
+ } catch (jsonError) {
+ this.jsonFormStatusMessage =
+ 'Invalid JSON\n' +
+ 'parser returned:\n\n' + jsonError;
+ return;
+ }
+ }
+
+ public toggleVisible(item: string) {
+ this.isVisible[item] = !this.isVisible[item];
+ }
+}
+
+export function getPolicyDialogProperties(policyType: PolicyType, instance: PolicyInstance): MatDialogConfig {
+ const policyTypeId = policyType.policy_type_id;
+ const createSchema = policyType.create_schema;
+ const instanceId = instance ? instance.instanceId : null;
+ const instanceJson = instance ? instance.instance : null;
+ const name = policyType.name;
+
+ return {
+ maxWidth: '1200px',
+ height: '1200px',
+ width: '900px',
+ role: 'dialog',
+ disableClose: false,
+ data: {
+ policyTypeId,
+ createSchema,
+ instanceId,
+ instanceJson,
+ name
+ }
+ };
+}
+
diff --git a/dashboard/webapp-frontend/src/app/policy-control/policy-instance.component.html b/dashboard/webapp-frontend/src/app/policy-control/policy-instance.component.html
new file mode 100644
index 0000000..60bfab2
--- /dev/null
+++ b/dashboard/webapp-frontend/src/app/policy-control/policy-instance.component.html
@@ -0,0 +1,57 @@
+<!--
+ ========================LICENSE_START=================================
+ O-RAN-SC
+ %%
+ Copyright (C) 2019 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===================================
+ -->
+<table #table mat-table class="instances-table mat-elevation-z8" matSort multiTemplateDataRows [dataSource]="instanceDataSource">
+
+ <ng-container matColumnDef="instanceId">
+ <mat-header-cell mat-sort-header *matHeaderCellDef >Instance</mat-header-cell>
+ <mat-cell *matCellDef="let element" (click)="modifyInstance(element)">{{element.instanceId}}
+ </mat-cell>
+ </ng-container>
+
+ <ng-container matColumnDef="action">
+ <mat-header-cell class="action-cell" *matHeaderCellDef>Action</mat-header-cell>
+ <mat-cell class="action-cell" *matCellDef="let instance">
+ <button mat-icon-button (click)="modifyInstance(instance)">
+ <mat-icon>edit</mat-icon>
+ </button>
+ <button mat-icon-button color="warn" (click)="deleteInstance(instance)">
+ <mat-icon>delete</mat-icon>
+ </button>
+ </mat-cell>
+ </ng-container>
+
+ <ng-container matColumnDef="noRecordsFound">
+ <mat-footer-cell *matFooterCellDef>No records found.</mat-footer-cell>
+ </ng-container>
+
+ <mat-header-row *matHeaderRowDef="['instanceId', 'action']"
+ [ngClass]="{'display-none': !this.hasInstances()}">
+ </mat-header-row>
+ <mat-row *matRowDef="let instance; columns: ['instanceId', 'action'];"></mat-row>
+
+ <mat-footer-row *matFooterRowDef="['noRecordsFound']" [ngClass]="{'display-none': this.hasInstances()}">
+ </mat-footer-row>
+
+</table>
+
+
+<div class="spinner-container" *ngIf="instanceDataSource.loading$ | async">
+ <mat-spinner diameter="50"></mat-spinner>
+</div>
\ No newline at end of file
diff --git a/dashboard/webapp-frontend/src/app/policy-control/policy-instance.component.scss b/dashboard/webapp-frontend/src/app/policy-control/policy-instance.component.scss
new file mode 100644
index 0000000..5c1d7c3
--- /dev/null
+++ b/dashboard/webapp-frontend/src/app/policy-control/policy-instance.component.scss
@@ -0,0 +1,41 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 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===================================
+ */
+
+.instances-table {
+ width: 60%; ;
+ min-height: 150px;
+ min-width: 600px;
+ margin-top: 10px;
+ margin-bottom: 10px;
+ background-color:rgb(233, 233, 240);
+}
+
+.action-cell {
+ display: flex;
+ justify-content: flex-end;
+}
+
+.display-none {
+ display: none;
+}
+
+.spinner-container mat-spinner {
+ margin: 0 auto 0 auto;
+}
\ No newline at end of file
diff --git a/dashboard/webapp-frontend/src/app/policy-control/policy-instance.component.ts b/dashboard/webapp-frontend/src/app/policy-control/policy-instance.component.ts
new file mode 100644
index 0000000..3544275
--- /dev/null
+++ b/dashboard/webapp-frontend/src/app/policy-control/policy-instance.component.ts
@@ -0,0 +1,113 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 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 { MatSort } from '@angular/material';
+import { Component, OnInit, ViewChild, Input, AfterViewInit } from '@angular/core';
+import { MatDialog } from '@angular/material/dialog';
+import { PolicyType } from '../interfaces/policy.types';
+import { PolicyInstanceDataSource } from './policy-instance.datasource';
+import { ErrorDialogService } from '../services/ui/error-dialog.service';
+import { NotificationService } from '../services/ui/notification.service';
+import { PolicyService } from '../services/policy/policy.service';
+import { ConfirmDialogService } from './../services/ui/confirm-dialog.service';
+import { PolicyInstance } from '../interfaces/policy.types';
+import { PolicyInstanceDialogComponent } from './policy-instance-dialog.component';
+import { getPolicyDialogProperties } from './policy-instance-dialog.component';
+import { HttpErrorResponse, HttpResponse } from '@angular/common/http';
+import { Observable } from 'rxjs';
+
+@Component({
+ selector: 'rd-policy-instance',
+ templateUrl: './policy-instance.component.html',
+ styleUrls: ['./policy-instance.component.scss']
+})
+
+
+export class PolicyInstanceComponent implements OnInit, AfterViewInit {
+ instanceDataSource: PolicyInstanceDataSource;
+ @Input() policyType: PolicyType;
+ @Input() expanded: Observable<boolean>;
+ @ViewChild(MatSort, { static: true }) sort: MatSort;
+
+ constructor(
+ private policySvc: PolicyService,
+ private dialog: MatDialog,
+ private errorDialogService: ErrorDialogService,
+ private notificationService: NotificationService,
+ private confirmDialogService: ConfirmDialogService) {
+ }
+
+ ngOnInit() {
+ this.instanceDataSource = new PolicyInstanceDataSource(this.policySvc, this.sort, this.notificationService, this.policyType);
+ this.expanded.subscribe((isExpanded: boolean) => this.onExpand(isExpanded));
+ }
+
+ ngAfterViewInit() {
+ this.instanceDataSource.sort = this.sort;
+ }
+
+ private onExpand(isExpanded: boolean) {
+ if (isExpanded) {
+ this.instanceDataSource.loadTable();
+ }
+ }
+
+ modifyInstance(instance: PolicyInstance): void {
+ this.policySvc.getPolicy(this.policyType.policy_type_id, instance.instanceId).subscribe(
+ (refreshedJson: any) => {
+ instance.instance = JSON.stringify(refreshedJson);
+ this.dialog.open(PolicyInstanceDialogComponent, getPolicyDialogProperties(this.policyType, instance));
+ },
+ (httpError: HttpErrorResponse) => {
+ this.notificationService.error('Could not refresh instance ' + httpError.message);
+ this.dialog.open(PolicyInstanceDialogComponent, getPolicyDialogProperties(this.policyType, instance));
+ }
+ );
+ }
+
+ hasInstances(): boolean {
+ return this.instanceDataSource.rowCount > 0;
+ }
+
+ deleteInstance(instance: PolicyInstance): void {
+ this.confirmDialogService
+ .openConfirmDialog('Are you sure you want to delete this policy instance?')
+ .afterClosed().subscribe(
+ (res: any) => {
+ if (res) {
+ this.policySvc.deletePolicy(this.policyType.policy_type_id, instance.instanceId)
+ .subscribe(
+ (response: HttpResponse<Object>) => {
+ switch (response.status) {
+ case 200:
+ this.notificationService.success('Delete succeeded!');
+ this.instanceDataSource.loadTable();
+ break;
+ default:
+ this.notificationService.warn('Delete failed.');
+ }
+ },
+ (error: HttpErrorResponse) => {
+ this.errorDialogService.displayError(error.message);
+ });
+ }
+ });
+ }
+}
diff --git a/dashboard/webapp-frontend/src/app/policy-control/policy-instance.datasource.ts b/dashboard/webapp-frontend/src/app/policy-control/policy-instance.datasource.ts
new file mode 100644
index 0000000..c74c9ab
--- /dev/null
+++ b/dashboard/webapp-frontend/src/app/policy-control/policy-instance.datasource.ts
@@ -0,0 +1,100 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 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 { DataSource } from '@angular/cdk/collections';
+import { HttpErrorResponse } from '@angular/common/http';
+import { MatSort } from '@angular/material';
+import { Observable } from 'rxjs/Observable';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+import { merge } from 'rxjs';
+import { of } from 'rxjs/observable/of';
+import { catchError, finalize, map } from 'rxjs/operators';
+import { PolicyInstance } from '../interfaces/policy.types';
+import { PolicyService } from '../services/policy/policy.service';
+import { NotificationService } from '../services/ui/notification.service';
+import { PolicyType } from '../interfaces/policy.types';
+
+export class PolicyInstanceDataSource extends DataSource<PolicyInstance> {
+
+ private policyInstanceSubject = new BehaviorSubject<PolicyInstance[]>([]);
+
+ private loadingSubject = new BehaviorSubject<boolean>(false);
+
+ public loading$ = this.loadingSubject.asObservable();
+
+ public rowCount = 1; // hide footer during intial load
+
+ constructor(
+ private policySvc: PolicyService,
+ public sort: MatSort,
+ private notificationService: NotificationService,
+ private policyType: PolicyType) {
+ super();
+ }
+
+ loadTable() {
+ this.loadingSubject.next(true);
+ this.policySvc.getPolicyInstances(this.policyType.policy_type_id)
+ .pipe(
+ catchError((her: HttpErrorResponse) => {
+ this.notificationService.error('Failed to get policy instances: ' + her.message);
+ return of([]);
+ }),
+ finalize(() => this.loadingSubject.next(false))
+ )
+ .subscribe((instances: PolicyInstance[]) => {
+ this.rowCount = instances.length;
+ this.policyInstanceSubject.next(instances);
+ });
+ }
+
+ connect(): Observable<PolicyInstance[]> {
+ const dataMutations = [
+ this.policyInstanceSubject.asObservable(),
+ this.sort.sortChange
+ ];
+ return merge(...dataMutations).pipe(map(() => {
+ return this.getSortedData([...this.policyInstanceSubject.getValue()]);
+ }));
+ }
+
+ disconnect(): void {
+ this.policyInstanceSubject.complete();
+ this.loadingSubject.complete();
+ }
+
+ private getSortedData(data: PolicyInstance[]) {
+ if (!this.sort || !this.sort.active || this.sort.direction === '') {
+ return data;
+ }
+
+ return data.sort((a, b) => {
+ const isAsc = this.sort.direction === 'asc';
+ switch (this.sort.active) {
+ case 'instanceId': return compare(a.instanceId, b.instanceId, isAsc);
+ default: return 0;
+ }
+ });
+ }
+}
+
+function compare(a: string, b: string, isAsc: boolean) {
+ return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
+}
diff --git a/dashboard/webapp-frontend/src/app/policy-control/policy-type.datasource.ts b/dashboard/webapp-frontend/src/app/policy-control/policy-type.datasource.ts
new file mode 100644
index 0000000..1b2b93e
--- /dev/null
+++ b/dashboard/webapp-frontend/src/app/policy-control/policy-type.datasource.ts
@@ -0,0 +1,97 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2019 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 { CollectionViewer, DataSource } from '@angular/cdk/collections';
+import { HttpErrorResponse } from '@angular/common/http';
+import { MatSort } from '@angular/material';
+import { Observable } from 'rxjs/Observable';
+import { BehaviorSubject } from 'rxjs/BehaviorSubject';
+import { merge } from 'rxjs';
+import { of } from 'rxjs/observable/of';
+import { catchError, finalize, map } from 'rxjs/operators';
+import { PolicyType } from '../interfaces/policy.types';
+import { PolicyService } from '../services/policy/policy.service';
+import { NotificationService } from '../services/ui/notification.service';
+
+export class PolicyTypeDataSource extends DataSource<PolicyType> {
+
+ private policyTypeSubject = new BehaviorSubject<PolicyType[]>([]);
+
+ private loadingSubject = new BehaviorSubject<boolean>(false);
+
+ public loading$ = this.loadingSubject.asObservable();
+
+ public rowCount = 1; // hide footer during intial load
+
+ constructor(private policySvc: PolicyService,
+ private sort: MatSort,
+ private notificationService: NotificationService) {
+ super();
+ }
+
+ loadTable() {
+ this.loadingSubject.next(true);
+ this.policySvc.getPolicyTypes()
+ .pipe(
+ catchError((her: HttpErrorResponse) => {
+ this.notificationService.error('Failed to get policy types: ' + her.message);
+ return of([]);
+ }),
+ finalize(() => this.loadingSubject.next(false))
+ )
+ .subscribe((types: PolicyType[]) => {
+ this.rowCount = types.length;
+ this.policyTypeSubject.next(types);
+ });
+ }
+
+ connect(collectionViewer: CollectionViewer): Observable<PolicyType[]> {
+ const dataMutations = [
+ this.policyTypeSubject.asObservable(),
+ this.sort.sortChange
+ ];
+ return merge(...dataMutations).pipe(map(() => {
+ return this.getSortedData([...this.policyTypeSubject.getValue()]);
+ }));
+ }
+
+ disconnect(collectionViewer: CollectionViewer): void {
+ this.policyTypeSubject.complete();
+ this.loadingSubject.complete();
+ }
+
+ private getSortedData(data: PolicyType[]) {
+ if (!this.sort.active || this.sort.direction === '') {
+ return data;
+ }
+
+ return data.sort((a, b) => {
+ const isAsc = this.sort.direction === 'asc';
+ switch (this.sort.active) {
+ case 'name': return compare(a.name, b.name, isAsc);
+ default: return 0;
+ }
+ });
+ }
+}
+
+function compare(a: any, b: any, isAsc: boolean) {
+ return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
+}