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);
+}