improve dashboard UI 72/2272/8
authorNicolas Hu <jh245g@att.com>
Mon, 20 Jan 2020 16:03:22 +0000 (11:03 -0500)
committerNicolas Hu <jh245g@att.com>
Wed, 22 Jan 2020 18:44:32 +0000 (13:44 -0500)
Add instance selector dialog component and service
Repair onSidenavClose method

Change-Id: I1a5806b170df698caeed9250c79be3e3d0011659
Signed-off-by: Jun (Nicolas) Hu <jh245g@att.com>
19 files changed:
docs/release-notes.rst
webapp-frontend/src/app/ac-xapp/ac-xapp.component.ts
webapp-frontend/src/app/app-configuration/app-configuration.component.ts
webapp-frontend/src/app/app-control/app-control.component.ts
webapp-frontend/src/app/caas-ingress/caas-ingress.component.ts
webapp-frontend/src/app/catalog/catalog.component.ts
webapp-frontend/src/app/footer/footer.component.html
webapp-frontend/src/app/navigation/sidenav-list/sidenav-list.component.ts
webapp-frontend/src/app/ran-control/ran-control.component.ts
webapp-frontend/src/app/rd.component.html
webapp-frontend/src/app/rd.component.scss
webapp-frontend/src/app/rd.component.ts
webapp-frontend/src/app/rd.module.ts
webapp-frontend/src/app/services/instance-selector/instance-selector.service.ts
webapp-frontend/src/app/services/ui/instance-selector-dialog.service.spec.ts [new file with mode: 0644]
webapp-frontend/src/app/services/ui/instance-selector-dialog.service.ts [new file with mode: 0644]
webapp-frontend/src/app/ui/instance-selector-dialog/instance-selector-dialog.component.html [new file with mode: 0644]
webapp-frontend/src/app/ui/instance-selector-dialog/instance-selector-dialog.component.spec.ts [new file with mode: 0644]
webapp-frontend/src/app/ui/instance-selector-dialog/instance-selector-dialog.component.ts [new file with mode: 0644]

index 55202c8..87fe784 100644 (file)
@@ -19,6 +19,8 @@ Version 2.0.0, 20 Jan 2020
 * Upgrade A1 Mediator API and submodule to tag 2.1.0
 * Upgrade App Manager API and submodule to tag 0.3.3
 * Upgrade E2 Manager API and submodule to tag 3.0.3
+* Add instance selector dialog component and service
+* Repair onSidenavClose method
 
 Version 1.3.0, 26 Nov 2019
 --------------------------
index b4e41ce..1f1d61e 100644 (file)
  */
 
 import { HttpErrorResponse } from '@angular/common/http';
-import { Component, OnInit, OnDestroy } from '@angular/core';
+import { Component, OnDestroy, OnInit } from '@angular/core';
 import { FormControl, FormGroup, Validators } from '@angular/forms';
+import { Subscription } from 'rxjs';
 import { ACAdmissionIntervalControl } from '../interfaces/ac-xapp.types';
+import { RicInstance } from '../interfaces/dashboard.types';
 import { ACXappService } from '../services/ac-xapp/ac-xapp.service';
-import { ErrorDialogService } from '../services/ui/error-dialog.service';
-import { NotificationService } from './../services/ui/notification.service';
-import { Subscription } from 'rxjs';
 import { InstanceSelectorService } from '../services/instance-selector/instance-selector.service';
+import { ErrorDialogService } from '../services/ui/error-dialog.service';
+import { NotificationService } from '../services/ui/notification.service';
 
 @Component({
   selector: 'rd-ac-xapp',
@@ -55,11 +56,11 @@ export class AcXappComponent implements OnInit, OnDestroy {
       trigger_threshold: new FormControl('', [Validators.required, Validators.min(1)])
     });
 
-    this.instanceChange = this.instanceSelectorService.getSelectedInstancekey().subscribe((instanceKey: string) => {
-      if (instanceKey) {
+    this.instanceChange = this.instanceSelectorService.getSelectedInstance().subscribe((instance: RicInstance) => {
+      if (instance.key) {
         // TODO: show pending action indicator
-        this.instanceKey = instanceKey;
-        this.acXappService.getPolicy(instanceKey).subscribe((res: ACAdmissionIntervalControl) => {
+        this.instanceKey = instance.key;
+        this.acXappService.getPolicy(instance.key).subscribe((res: ACAdmissionIntervalControl) => {
           this.acForm.controls['class'].setValue(res.class);
           this.acForm.controls['enforce'].setValue(res.enforce);
           this.acForm.controls['window_length'].setValue(res.window_length);
index 406c350..0e73413 100644 (file)
@@ -26,7 +26,7 @@ import { AppMgrService } from '../services/app-mgr/app-mgr.service';
 import { ErrorDialogService } from '../services/ui/error-dialog.service';
 import { LoadingDialogService } from '../services/ui/loading-dialog.service';
 import { HttpErrorResponse, HttpResponse } from '@angular/common/http';
-import { NotificationService } from './../services/ui/notification.service';
+import { NotificationService } from '../services/ui/notification.service';
 
 @Component({
   selector: 'rd-app-configuration',
index 7b1a1b4..fd2b26c 100644 (file)
@@ -24,6 +24,7 @@ import { Router } from '@angular/router';
 import { Subscription } from 'rxjs';
 import { finalize } from 'rxjs/operators';
 import { XappControlRow } from '../interfaces/app-mgr.types';
+import { RicInstance } from '../interfaces/dashboard.types';
 import { AppMgrService } from '../services/app-mgr/app-mgr.service';
 import { InstanceSelectorService } from '../services/instance-selector/instance-selector.service';
 import { ConfirmDialogService } from '../services/ui/confirm-dialog.service';
@@ -59,10 +60,10 @@ export class AppControlComponent implements OnInit, OnDestroy {
 
   ngOnInit() {
     this.dataSource = new AppControlDataSource(this.appMgrSvc, this.sort, this.notificationService);
-    this.instanceChange = this.instanceSelectorService.getSelectedInstancekey().subscribe((instanceKey: string) => {
-      if (instanceKey) {
-        this.instanceKey = instanceKey;
-        this.dataSource.loadTable(instanceKey);
+    this.instanceChange = this.instanceSelectorService.getSelectedInstance().subscribe((instance: RicInstance) => {
+      if (instance.key) {
+        this.instanceKey = instance.key;
+        this.dataSource.loadTable(instance.key);
       }
     });
   }
index c389cc9..13ff7af 100644 (file)
@@ -21,6 +21,7 @@
 import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
 import { MatSort } from '@angular/material/sort';
 import { Subscription } from 'rxjs';
+import { RicInstance } from '../interfaces/dashboard.types';
 import { CaasIngressService } from '../services/caas-ingress/caas-ingress.service';
 import { InstanceSelectorService } from '../services/instance-selector/instance-selector.service';
 import { ConfirmDialogService } from '../services/ui/confirm-dialog.service';
@@ -56,9 +57,9 @@ export class CaasIngressComponent implements OnInit, OnDestroy {
 
   ngOnInit() {
     this.dataSource = new CaasIngressDataSource(this.caasIngressSvc, this.sort, this.notificationService);
-    this.instanceChange = this.instanceSelectorService.getSelectedInstancekey().subscribe((instanceKey: string) => {
-      if (instanceKey) {
-        this.dataSource.loadTable(instanceKey, this.cluster, this.namespace);
+    this.instanceChange = this.instanceSelectorService.getSelectedInstance().subscribe((instance: RicInstance) => {
+      if (instance.key) {
+        this.dataSource.loadTable(instance.key, this.cluster, this.namespace);
       }
     });
   }
index d46c99f..ad90b36 100644 (file)
@@ -23,14 +23,15 @@ import { MatDialog } from '@angular/material/dialog';
 import { MatSort } from '@angular/material/sort';
 import { Subscription } from 'rxjs';
 import { finalize } from 'rxjs/operators';
+import { RicInstance } from '../interfaces/dashboard.types';
 import { XMDeployableApp } from '../interfaces/app-mgr.types';
 import { AppMgrService } from '../services/app-mgr/app-mgr.service';
 import { InstanceSelectorService } from '../services/instance-selector/instance-selector.service';
 import { LoadingDialogService } from '../services/ui/loading-dialog.service';
 import { UiService } from '../services/ui/ui.service';
 import { AppConfigurationComponent } from './../app-configuration/app-configuration.component';
-import { ConfirmDialogService } from './../services/ui/confirm-dialog.service';
-import { NotificationService } from './../services/ui/notification.service';
+import { ConfirmDialogService } from '../services/ui/confirm-dialog.service';
+import { NotificationService } from '../services/ui/notification.service';
 import { CatalogDataSource } from './catalog.datasource';
 
 @Component({
@@ -64,10 +65,10 @@ export class CatalogComponent implements OnInit, OnDestroy {
       this.darkMode = isDark;
     });
 
-    this.instanceChange = this.instanceSelectorService.getSelectedInstancekey().subscribe((instanceKey: string) => {
-      if (instanceKey) {
-        this.instanceKey = instanceKey;
-        this.dataSource.loadTable(instanceKey);
+    this.instanceChange = this.instanceSelectorService.getSelectedInstance().subscribe((instance: RicInstance) => {
+      if (instance.key) {
+        this.instanceKey = instance.key;
+        this.dataSource.loadTable(instance.key);
       }
     });
   }
index 5dd1c36..7d78f92 100644 (file)
@@ -18,7 +18,7 @@
   ========================LICENSE_END===================================
   -->
 <div class="copyright__text" [ngClass]="{'footer-dark': darkMode}">
-  Copyright (C) 2019 AT&T Intellectual Property. Licensed under the Apache License, Version 2.0.
+  Copyright (C) 2020 AT&T Intellectual Property. Licensed under the Apache License, Version 2.0.
   <br/>
   Version {{dashboardVersion}}
 </div>
index c0f05af..f8b23d0 100644 (file)
@@ -27,7 +27,7 @@ import { UiService } from '../../services/ui/ui.service';
 })
 export class SidenavListComponent implements OnInit {
   darkMode: boolean;
-  @Output() sidenavClose = new EventEmitter();
+  @Output() sidenavClose: EventEmitter<any>  = new EventEmitter();
 
   constructor(public ui: UiService) { }
 
index 1cffdd8..ac995f0 100644 (file)
@@ -22,6 +22,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
 import { MatDialog } from '@angular/material/dialog';
 import { Subscription } from 'rxjs';
 import { finalize } from 'rxjs/operators';
+import { RicInstance } from '../interfaces/dashboard.types';
 import { E2ManagerService } from '../services/e2-mgr/e2-mgr.service';
 import { InstanceSelectorService } from '../services/instance-selector/instance-selector.service';
 import { ConfirmDialogService } from '../services/ui/confirm-dialog.service';
@@ -62,10 +63,10 @@ export class RanControlComponent implements OnInit, OnDestroy {
       this.darkMode = isDark;
     });
 
-    this.instanceChange = this.instanceSelectorService.getSelectedInstancekey().subscribe((instanceKey: string) => {
-      if (instanceKey) {
-        this.instanceKey = instanceKey;
-        this.dataSource.loadTable(instanceKey);
+    this.instanceChange = this.instanceSelectorService.getSelectedInstance().subscribe((instance: RicInstance) => {
+      if (instance.key) {
+        this.instanceKey = instance.key;
+        this.dataSource.loadTable(instance.key);
       }
     });
   }
index 85cbbcd..61e0b45 100644 (file)
@@ -7,9 +7,9 @@
   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.
   ========================LICENSE_END===================================
   -->
 <!-- Slide Menu-->
-<mat-drawer-container autosize >
+<mat-drawer-container autosize [hasBackdrop]=true>
   <mat-drawer #drawer mode="push" [ngClass]="{'side-menu__dark': darkModeActive}">
-  <section class="menu-header" [ngClass]="{'menu-header__dark': darkModeActive}">
-
-    <div class="left__section3Col" *ngIf="drawer.opened"  [ngClass]="{'menumargin-top': drawer.opened}">
-      <svg (click)="drawer.toggle()" class="hamburger__icon" id="Menu_Burger_Icon" data-name="Menu Burger Icon"
-           viewBox="31.5 30 49.9 32">
-        <rect id="Rectangle_9" width="49.9" height="4" [ngClass]="{'hamburger__icon__fill-dark': darkModeActive}"
-              class="hamburger__icon__fill" data-name="Rectangle 9" rx="2"
-              transform="translate(31.5 58)"/>
-        <rect id="Rectangle_10" width="49.9" height="4" [ngClass]="{'hamburger__icon__fill-dark': darkModeActive}"
-              class="hamburger__icon__fill" data-name="Rectangle 10" rx="2"
-              transform="translate(31.5 44)"/>
-        <rect id="Rectangle_11" width="49.9" height="4" [ngClass]="{'hamburger__icon__fill-dark': darkModeActive}"
-              class="hamburger__icon__fill" data-name="Rectangle 11" rx="2"
-              transform="translate(31.5 30)"/>
-      </svg>
-      <img src="../../assets/oran-logo.png" width="180%" height="100%" style="position: relative; z-index: 50"/>
-      <svg class="logo__icon" viewBox="150.3 22.2 313.7 42.8">
-        <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 x="0" y="0">RIC Dashboard</tspan>
-        </text>
-      </svg>
+    <section class="menu-header" [ngClass]="{'menu-header__dark': darkModeActive}">
+      <div class="left__section3Col" *ngIf="drawer.opened" [ngClass]="{'menumargin-top': drawer.opened}">
+        <svg (click)="drawer.toggle()" class="hamburger__icon" id="Menu_Burger_Icon" data-name="Menu Burger Icon"
+             viewBox="31.5 30 49.9 32">
+          <rect id="Rectangle_9" width="49.9" height="4" [ngClass]="{'hamburger__icon__fill-dark': darkModeActive}"
+                class="hamburger__icon__fill" data-name="Rectangle 9" rx="2"
+                transform="translate(31.5 58)" />
+          <rect id="Rectangle_10" width="49.9" height="4" [ngClass]="{'hamburger__icon__fill-dark': darkModeActive}"
+                class="hamburger__icon__fill" data-name="Rectangle 10" rx="2"
+                transform="translate(31.5 44)" />
+          <rect id="Rectangle_11" width="49.9" height="4" [ngClass]="{'hamburger__icon__fill-dark': darkModeActive}"
+                class="hamburger__icon__fill" data-name="Rectangle 11" rx="2"
+                transform="translate(31.5 30)" />
+        </svg>
+        <img src="../../assets/oran-logo.png" width="180%" height="100%" style="position: relative; z-index: 50" />
+        <svg class="logo__icon" viewBox="150.3 22.2 313.7 42.8">
+          <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 x="0" y="0">RIC Dashboard</tspan>
+          </text>
+        </svg>
 
-    </div>
+      </div>
 
       <div class="profile-image__container">
         <img src="assets/profile_default.png" alt="profile-image"
         <span class="email__text">demo@o-ran-sc.org</span>
       </div>
     </section>
-    <section #sidenav class="menu-body" >
-       <rd-sidenav-list ></rd-sidenav-list>
+    <section #sidenav class="menu-body">
+      <rd-sidenav-list (sidenavClose)="drawer.toggle()"></rd-sidenav-list>
     </section>
     <section class="menu-footer">
-
+      <mat-nav-list [ngClass]="{'dark': darkModeActive}">
+        <a mat-list-item (click)="openInstanceSelectorDialog(); drawer.toggle()">
+          <mat-icon>link</mat-icon> <span class="nav-caption">{{selectedInstanceName}}</span>
+        </a>
+      </mat-nav-list>
     </section>
-</mat-drawer>
+  </mat-drawer>
 
-<mat-drawer-content>
-<div class="root__container">
+  <mat-drawer-content>
+    <div class="root__container">
 
-  <header [ngClass]="{'main__header-dark': darkModeActive}" class="main__header">
+      <header [ngClass]="{'main__header-dark': darkModeActive}" class="main__header">
 
-    <div class="left__section3Col" *ngIf="!drawer.opened">
-      <svg (click)="drawer.toggle()" class="hamburger__icon" id="Menu_Burger_Icon" data-name="Menu Burger Icon"
-           viewBox="31.5 30 49.9 32">
-        <rect id="Rectangle_9" width="49.9" height="4" [ngClass]="{'hamburger__icon__fill-dark': darkModeActive}"
-              class="hamburger__icon__fill" data-name="Rectangle 9" rx="2"
-              transform="translate(31.5 58)" />
-        <rect id="Rectangle_10" width="49.9" height="4" [ngClass]="{'hamburger__icon__fill-dark': darkModeActive}"
-              class="hamburger__icon__fill" data-name="Rectangle 10" rx="2"
-              transform="translate(31.5 44)" />
-        <rect id="Rectangle_11" width="49.9" height="4" [ngClass]="{'hamburger__icon__fill-dark': darkModeActive}"
-              class="hamburger__icon__fill" data-name="Rectangle 11" rx="2"
-              transform="translate(31.5 30)" />
-      </svg>
-      <img src="../../assets/oran-logo.png" width="180%" height="100%" style="position: relative; z-index: 50" />
-      <svg class="logo__icon" viewBox="150.3 22.2 313.7 42.8">
-        <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 x="0" y="0">RIC Dashboard</tspan>
-        </text>
-      </svg>
+        <div class="left__section3Col" *ngIf="!drawer.opened">
+          <svg (click)="drawer.toggle()" class="hamburger__icon" id="Menu_Burger_Icon" data-name="Menu Burger Icon"
+               viewBox="31.5 30 49.9 32">
+            <rect id="Rectangle_9" width="49.9" height="4" [ngClass]="{'hamburger__icon__fill-dark': darkModeActive}"
+                  class="hamburger__icon__fill" data-name="Rectangle 9" rx="2"
+                  transform="translate(31.5 58)" />
+            <rect id="Rectangle_10" width="49.9" height="4" [ngClass]="{'hamburger__icon__fill-dark': darkModeActive}"
+                  class="hamburger__icon__fill" data-name="Rectangle 10" rx="2"
+                  transform="translate(31.5 44)" />
+            <rect id="Rectangle_11" width="49.9" height="4" [ngClass]="{'hamburger__icon__fill-dark': darkModeActive}"
+                  class="hamburger__icon__fill" data-name="Rectangle 11" rx="2"
+                  transform="translate(31.5 30)" />
+          </svg>
+          <img src="../../assets/oran-logo.png" width="180%" height="100%" style="position: relative; z-index: 50" />
+          <svg class="logo__icon" viewBox="150.3 22.2 313.7 42.8">
+            <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 x="0" y="0">RIC Dashboard</tspan>
+            </text>
+          </svg>
 
-    </div>
+        </div>
 
-    <div>
-      <mat-label class="selector-label">Select RIC Instance</mat-label>
-      <mat-form-field>
-        <mat-select (selectionChange)="changeInstance($event.value)" [(value)]="selectedInstanceKey">
-          <mat-option *ngFor="let instance of instanceArray" [value]="instance.key">
-            {{instance.name}}
-          </mat-option>
-        </mat-select>
-      </mat-form-field>
-    </div>
+        <div>    </div>
 
-    <div class="mode-toggle__container">
-      <span class="mode-toggle__text">Light</span>
-      <label class="toggle-button__container">
-        <input (click)="modeToggleSwitch()" type="checkbox" class="mode-toggle__input" />
-        <span [ngClass]="{'mode-toggle__bg-checked': darkModeActive}" class="mode-toggle__bg"></span>
-        <span [ngClass]="{'mode-toggle__circle-checked': darkModeActive}" class="mode-toggle__circle"></span>
-      </label>
-      <span class="mode-toggle__text">Dark</span>
-    </div>
+        <div class="mode-toggle__container">
+          <span class="mode-toggle__text">Light</span>
+          <label class="toggle-button__container">
+            <input (click)="modeToggleSwitch()" type="checkbox" class="mode-toggle__input" />
+            <span [ngClass]="{'mode-toggle__bg-checked': darkModeActive}" class="mode-toggle__bg"></span>
+            <span [ngClass]="{'mode-toggle__circle-checked': darkModeActive}" class="mode-toggle__circle"></span>
+          </label>
+          <span class="mode-toggle__text">Dark</span>
+        </div>
 
-  </header>
+      </header>
 
-  <!-- Main Content -->
-  <main class="main__container" [ngClass]="{'main-container__bg-dark': darkModeActive}" >
-    <div class = main__container__body [ngClass]="{'dark-theme': darkModeActive}">
-      <div class="main-container__bg"></div>
-      <router-outlet></router-outlet>
-    </div>
-  </main>
+      <!-- Main Content -->
+      <main class="main__container" [ngClass]="{'main-container__bg-dark': darkModeActive}">
+        <div class=main__container__body [ngClass]="{'dark-theme': darkModeActive}">
+          <div class="main-container__bg"></div>
+          <router-outlet></router-outlet>
+        </div>
+      </main>
 
-  <!-- Footer -->
-  <footer class="main__footer" [ngClass]="{'main-footer__bg-dark': darkModeActive}">
-    <div class="main-footer__bg">
-      <rd-footer></rd-footer>
-    </div>
-  </footer>
+      <!-- Footer -->
+      <footer class="main__footer" [ngClass]="{'main-footer__bg-dark': darkModeActive}">
+        <div class="main-footer-licence">
+          <rd-footer></rd-footer>
+        </div>
+        <button mat-flat-button  color="primary" class="main-footer-instance" (click)="openInstanceSelectorDialog()" >
+          <span> {{selectedInstanceName}} </span>
+        </button>
+      </footer>
 
-</div>
-</mat-drawer-content>
+    </div>
+  </mat-drawer-content>
 </mat-drawer-container>
index 7245077..35fbb1b 100644 (file)
 
 /* Header */
 mat-sidenav-container, mat-sidenav-content, mat-sidenav {
-    height: 100%;
+  height: 100%;
 }
 
 mat-sidenav {
-    width: 250px;
+  width: 250px;
 }
 
 main {
-    padding: 10px;
+  padding: 10px;
 }
 
 /* Slide Menu */
@@ -102,6 +102,7 @@ main {
 .slide-menu-active {
   transform: none;
 }
+
 .menu-header.menu-header__dark {
   background: #2B244D;
 }
@@ -128,7 +129,7 @@ mat-drawer-content {
 }
 
 .menumargin-top {
-    margin-top: 10px;
+  margin-top: 10px;
 }
 
 .greeting__text {
@@ -183,12 +184,12 @@ mat-drawer-content {
   max-width: 4rem;
 }
 
-.home_bg_image{
-    height:40em;
-    background-size:cover;
-    width:auto;
-    background-image:url('../assets/intelligence.png');
-    background-position:50% 50%;
+.home_bg_image {
+  height: 40em;
+  background-size: cover;
+  width: auto;
+  background-image: url('../assets/intelligence.png');
+  background-position: 50% 50%;
 }
 
 /*Header*/
@@ -352,12 +353,28 @@ mat-drawer-content {
   color: white;
 }
 
+.main-footer-licence {
+  float: left;
+}
+
 .main-footer__bg-dark {
   opacity: 1;
   background: linear-gradient(to bottom, #B290FF, #2E1D65);
   transition: opacity 300ms linear;
 }
 
+.main-footer-instance {
+  margin-right: 10px;
+  float: right;
+  color: white;
+  letter-spacing: 0.1rem;
+  font-size: 15px;
+}
+
+.main-footer-instance:hover {
+  color: lightgray;
+}
+
 @media only screen and (max-width: 300px) {
   .slide-menu {
     width: 100%;
@@ -365,5 +382,30 @@ mat-drawer-content {
 }
 
 .selector-label {
-    margin-right: 20px;
-}
\ No newline at end of file
+  margin-right: 20px;
+}
+
+.menu-footer {
+  display: grid;
+  width: 100%;
+  position: absolute;
+  bottom: 10px;
+}
+
+a {
+  text-decoration: none;
+  color: black;
+}
+
+.dark a {
+  color: white;
+}
+
+a:hover, a:active {
+  color: lightgray;
+}
+
+.nav-caption {
+  display: inline-block;
+  padding-left: 6px;
+}
index 16a1849..31bd764 100644 (file)
  * ========================LICENSE_END===================================
  */
 import { Component, OnInit } from '@angular/core';
-import { finalize } from 'rxjs/operators';
+import { Subscription } from 'rxjs';
 import { RicInstance } from './interfaces/dashboard.types';
 import { InstanceSelectorService } from './services/instance-selector/instance-selector.service';
-import { LoadingDialogService } from './services/ui/loading-dialog.service';
+import { InstanceSelectorDialogService } from './services/ui/instance-selector-dialog.service';
 import { UiService } from './services/ui/ui.service';
 
 @Component({
@@ -32,40 +32,40 @@ import { UiService } from './services/ui/ui.service';
 export class RdComponent implements OnInit {
   showMenu = false;
   darkModeActive: boolean;
-  private instanceArray: RicInstance[];
-  private selectedInstanceKey: string;
+
+  private selectedInstanceName: string = 'Select RIC instance';
+  private instanceChange: Subscription;
 
   constructor(
     public ui: UiService,
-    private instanceSelectorService: InstanceSelectorService,
-    private loadingDialogService: LoadingDialogService) {
+    private instanceSelectorDialogService: InstanceSelectorDialogService,
+    private instanceSelectorService: InstanceSelectorService) {
   }
 
   ngOnInit() {
     this.ui.darkModeState.subscribe((value) => {
       this.darkModeActive = value;
     });
-    this.loadingDialogService.startLoading('Loading RIC instances');
-    this.instanceSelectorService.getInstanceArray()
-      .pipe(
-        finalize(() => this.loadingDialogService.stopLoading())
-      )
-      .subscribe((instanceArray: RicInstance[]) => {
-        this.instanceArray = instanceArray;
-        this.selectedInstanceKey = instanceArray[0].key;
-      })
+
+    this.instanceChange = this.instanceSelectorService.getSelectedInstance().subscribe((instance: RicInstance) => {
+      if (instance.name) {
+        this.selectedInstanceName = instance.name;
+      } else {
+        this.openInstanceSelectorDialog()
+      }
+    });
   }
 
-  toggleMenu() {
-    this.showMenu = !this.showMenu;
+  ngOnDestroy() {
+    this.instanceChange.unsubscribe();
   }
 
   modeToggleSwitch() {
     this.ui.darkModeState.next(!this.darkModeActive);
   }
 
-  changeInstance(selectedInstancekey: string) {
-    this.instanceSelectorService.updateSelectedInstance(selectedInstancekey);
+  openInstanceSelectorDialog() {
+    this.instanceSelectorDialogService.openInstanceSelectorDialog();
   }
 
 }
index 98e50c9..0a1e2bf 100644 (file)
@@ -67,6 +67,7 @@ import { ControlComponent } from './control/control.component';
 import { EditDashboardUserDialogComponent } from './user/edit-dashboard-user-dialog/edit-dashboard-user-dialog.component';
 import { ErrorDialogComponent } from './ui/error-dialog/error-dialog.component';
 import { FooterComponent } from './footer/footer.component';
+import { InstanceSelectorDialogComponent } from './ui/instance-selector-dialog/instance-selector-dialog.component';
 import { LoadingDialogComponent } from './ui/loading-dialog/loading-dialog.component';
 import { MainComponent } from './main/main.component';
 import { PlatformComponent } from './platform/platform.component';
@@ -85,8 +86,10 @@ import { DashboardService } from './services/dashboard/dashboard.service';
 import { E2ManagerService } from './services/e2-mgr/e2-mgr.service';
 import { ErrorDialogService } from './services/ui/error-dialog.service';
 import { InstanceSelectorService } from './services/instance-selector/instance-selector.service';
+import { InstanceSelectorDialogService } from './services/ui/instance-selector-dialog.service';
 import { UiService } from './services/ui/ui.service';
 
+
 @NgModule({
   declarations: [
     AcXappComponent,
@@ -111,7 +114,8 @@ import { UiService } from './services/ui/ui.service';
     SidenavListComponent,
     StatCardComponent,
     StatsComponent,
-    UserComponent
+    UserComponent,
+    InstanceSelectorDialogComponent
   ],
   imports: [
     BrowserModule,
@@ -174,6 +178,7 @@ import { UiService } from './services/ui/ui.service';
     ConfirmDialogComponent,
     EditDashboardUserDialogComponent,
     ErrorDialogComponent,
+    InstanceSelectorDialogComponent,
     LoadingDialogComponent,
     RanControlConnectDialogComponent
   ],
@@ -183,6 +188,7 @@ import { UiService } from './services/ui/ui.service';
     E2ManagerService,
     ErrorDialogService,
     InstanceSelectorService,
+    InstanceSelectorDialogService,
     UiService
   ],
   bootstrap: [RdComponent]
index cf004f7..fbc6bbb 100644 (file)
@@ -29,8 +29,9 @@ import { DashboardService } from '../dashboard/dashboard.service';
   providedIn: 'root'
 })
 export class InstanceSelectorService {
-  private selectedInstanceKey: BehaviorSubject<string> = new BehaviorSubject<string>('');
+
   private instanceArray: Observable<RicInstance[]>;
+  private selectedInstance: BehaviorSubject<RicInstance> = new BehaviorSubject<RicInstance>({ key: '', name:''});
 
   constructor(
     private dashboardSvc: DashboardService,
@@ -44,28 +45,19 @@ export class InstanceSelectorService {
     const path = this.dashboardSvc.buildPath('admin', null, 'instance');
     return this.instanceArray = this.httpClient.get<RicInstance[]>(path)
       .pipe(
-        tap(ricInstanceArray => {
-          this.initselectedInstanceKey(ricInstanceArray[0].key);
-        }),
         shareReplay(1)
       );
   }
 
-  private initselectedInstanceKey(instanceKey: string) {
-    if (!this.selectedInstanceKey.value) {
-      this.selectedInstanceKey.next(instanceKey)
-    }
-  }
-
-  // This method may return the BehaviorSubject with empty string
+  // This method may return the BehaviorSubject with empty value
   // Afther subscribe that BehaviorSubject
   // Please make sure this BehaviorSubject has non empty value
-  getSelectedInstancekey(): BehaviorSubject<string> {
-    return this.selectedInstanceKey;
+  getSelectedInstance(): BehaviorSubject<RicInstance> {
+    return this.selectedInstance;
   }
 
-  updateSelectedInstance(instanceKey: string) {
-    this.selectedInstanceKey.next(instanceKey)
+  updateSelectedInstance(instance: RicInstance) {
+    this.selectedInstance.next(instance)
   }
 
 }
diff --git a/webapp-frontend/src/app/services/ui/instance-selector-dialog.service.spec.ts b/webapp-frontend/src/app/services/ui/instance-selector-dialog.service.spec.ts
new file mode 100644 (file)
index 0000000..c812635
--- /dev/null
@@ -0,0 +1,31 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2020 AT&T Intellectual Property
+ * %%
+ * 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 { TestBed } from '@angular/core/testing';
+
+import { InstanceSelectorDialogService } from './instance-selector-dialog.service';
+
+describe('InstanceSelectorDialogService', () => {
+  beforeEach(() => TestBed.configureTestingModule({}));
+
+  it('should be created', () => {
+    const service: InstanceSelectorDialogService = TestBed.get(InstanceSelectorDialogService);
+    expect(service).toBeTruthy();
+  });
+});
diff --git a/webapp-frontend/src/app/services/ui/instance-selector-dialog.service.ts b/webapp-frontend/src/app/services/ui/instance-selector-dialog.service.ts
new file mode 100644 (file)
index 0000000..e723916
--- /dev/null
@@ -0,0 +1,52 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2020 AT&T Intellectual Property
+ * %%
+ * 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 { Injectable } from '@angular/core';
+import { MatDialog } from '@angular/material/dialog';
+import { InstanceSelectorDialogComponent } from '../../ui/instance-selector-dialog/instance-selector-dialog.component';
+import { UiService } from './ui.service';
+
+
+@Injectable({
+  providedIn: 'root'
+})
+export class InstanceSelectorDialogService {
+
+  darkMode: boolean;
+  panelClass: string;
+
+  constructor(private dialog: MatDialog,
+    public ui: UiService) { }
+
+  openInstanceSelectorDialog() {
+    this.ui.darkModeState.subscribe((isDark) => {
+      this.darkMode = isDark;
+    });
+    if (this.darkMode) {
+      this.panelClass = 'dark-theme';
+    } else {
+      this.panelClass = '';
+    }
+    return this.dialog.open(InstanceSelectorDialogComponent, {
+      panelClass: this.panelClass,
+      width: '480px',
+      position: { top: '100px' },
+    });
+  }
+}
diff --git a/webapp-frontend/src/app/ui/instance-selector-dialog/instance-selector-dialog.component.html b/webapp-frontend/src/app/ui/instance-selector-dialog/instance-selector-dialog.component.html
new file mode 100644 (file)
index 0000000..2b9d22a
--- /dev/null
@@ -0,0 +1,39 @@
+<!--
+  ========================LICENSE_START=================================
+  O-RAN-SC
+  %%
+  Copyright (C) 2020 AT&T Intellectual Property
+  %%
+  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 mat-dialog-title>
+  Select RIC Instance
+</div>
+<form [formGroup]="instanceForm" novalidate autocomplete="off" (ngSubmit)="changeInstance(instanceForm.value.instance)">
+  <div mat-dialog-content>
+    <mat-form-field>
+      <mat-label>Select an instance</mat-label>
+      <mat-select formControlName="instance">
+        <mat-option *ngFor="let instance of instanceArray" [value]="instance">
+          {{instance.name}}
+        </mat-option>
+      </mat-select>
+    </mat-form-field>
+  </div>
+  <div mat-dialog-actions class="modal-footer justify-content-center">
+    <button mat-button class="mat-raised-button" [mat-dialog-close]="false">Cancel</button>
+    <button mat-button class="mat-raised-button mat-primary" [disabled]="!instanceForm.valid">OK</button>
+  </div>
+</form>
\ No newline at end of file
diff --git a/webapp-frontend/src/app/ui/instance-selector-dialog/instance-selector-dialog.component.spec.ts b/webapp-frontend/src/app/ui/instance-selector-dialog/instance-selector-dialog.component.spec.ts
new file mode 100644 (file)
index 0000000..193d10b
--- /dev/null
@@ -0,0 +1,45 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2020 AT&T Intellectual Property
+ * %%
+ * 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 { InstanceSelectorDialogComponent } from './instance-selector-dialog.component';
+
+describe('InstanceSelectorDialogComponent', () => {
+  let component: InstanceSelectorDialogComponent;
+  let fixture: ComponentFixture<InstanceSelectorDialogComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ InstanceSelectorDialogComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(InstanceSelectorDialogComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/webapp-frontend/src/app/ui/instance-selector-dialog/instance-selector-dialog.component.ts b/webapp-frontend/src/app/ui/instance-selector-dialog/instance-selector-dialog.component.ts
new file mode 100644 (file)
index 0000000..8c6159c
--- /dev/null
@@ -0,0 +1,75 @@
+/*-
+ * ========================LICENSE_START=================================
+ * O-RAN-SC
+ * %%
+ * Copyright (C) 2020 AT&T Intellectual Property
+ * %%
+ * 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, OnDestroy, OnInit } from '@angular/core';
+import { FormControl, FormGroup, Validators } from '@angular/forms';
+import { MatDialogRef } from '@angular/material/dialog';
+import { Subscription } from 'rxjs';
+import { finalize } from 'rxjs/operators';
+import { RicInstance } from '../../interfaces/dashboard.types';
+import { InstanceSelectorService } from '../../services/instance-selector/instance-selector.service';
+import { LoadingDialogService } from '../../services/ui/loading-dialog.service';
+
+@Component({
+  selector: 'rd-instance-selector-dialog',
+  templateUrl: './instance-selector-dialog.component.html',
+})
+export class InstanceSelectorDialogComponent implements OnInit, OnDestroy  {
+
+  private instanceArray: RicInstance[];
+  private instanceForm: FormGroup;
+  private instanceChange: Subscription;
+
+  constructor(
+    private dialogRef: MatDialogRef<InstanceSelectorDialogComponent>,
+    private instanceSelectorService: InstanceSelectorService,
+    private loadingDialogService: LoadingDialogService) { }
+
+  ngOnInit() {
+    this.instanceForm = new FormGroup({
+      instance: new FormControl('', [Validators.required]),
+    })
+
+    this.loadingDialogService.startLoading('Loading RIC instances');
+    this.instanceSelectorService.getInstanceArray()
+      .pipe(
+        finalize(() => this.loadingDialogService.stopLoading())
+      )
+      .subscribe((instanceArray: RicInstance[]) => {
+        this.instanceArray = instanceArray;
+      })
+
+    this.instanceChange = this.instanceSelectorService.getSelectedInstance().subscribe((selectedInstance: RicInstance) => {
+      if (selectedInstance.key) {
+        this.instanceForm.setValue({ instance: selectedInstance })
+      }
+    });
+  }
+
+  ngOnDestroy() {
+    this.instanceChange.unsubscribe();
+  }
+
+  changeInstance(selectedInstance) {
+    this.instanceSelectorService.updateSelectedInstance(selectedInstance);
+    this.dialogRef.close(true);
+  }
+
+}