You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dlab.apache.org by dg...@apache.org on 2020/03/10 12:38:42 UTC

[incubator-dlab] branch develop updated: [DLAB-384]: Grouped roles (#642)

This is an automated email from the ASF dual-hosted git repository.

dgnatyshyn pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/incubator-dlab.git


The following commit(s) were added to refs/heads/develop by this push:
     new e2aa6f9  [DLAB-384]: Grouped roles (#642)
e2aa6f9 is described below

commit e2aa6f9351ecc0c18ba5f9077831cacc1728be1b
Author: Dmytro Gnatyshyn <42...@users.noreply.github.com>
AuthorDate: Tue Mar 10 14:38:35 2020 +0200

    [DLAB-384]: Grouped roles (#642)
    
     [DLAB-384]: Grouped roles
---
 .../epam/dlab/backendapi/dao/UserRoleDaoImpl.java  |   2 +
 .../dlab/backendapi/resources/dto/UserRoleDto.java |  10 +-
 .../src/main/resources/mongo/gcp/mongo_roles.json  |  26 ++
 .../app/administration/roles/roles.component.html  |  64 ++---
 .../app/administration/roles/roles.component.scss  |  15 +-
 .../app/administration/roles/roles.component.ts    |  29 +-
 .../webapp/src/app/shared/form-controls/index.ts   |   5 +-
 .../multi-level-select-dropdown.component.html     |  70 +++++
 .../multi-level-select-dropdown.component.scss     | 319 +++++++++++++++++++++
 .../multi-level-select-dropdown.component.ts       | 101 +++++++
 .../resources/webapp/src/assets/styles/_theme.scss |   6 +
 11 files changed, 582 insertions(+), 65 deletions(-)

diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/dao/UserRoleDaoImpl.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/dao/UserRoleDaoImpl.java
index 0767be4..f4deb0a 100644
--- a/services/self-service/src/main/java/com/epam/dlab/backendapi/dao/UserRoleDaoImpl.java
+++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/dao/UserRoleDaoImpl.java
@@ -55,6 +55,7 @@ public class UserRoleDaoImpl extends BaseDAO implements UserRoleDao {
 	private static final String USERS_FIELD = "users";
 	private static final String GROUPS_FIELD = "groups";
 	private static final String DESCRIPTION = "description";
+	private static final String TYPE = "type";
 	private static final String ROLES = "roles";
 	private static final String GROUPS = "$groups";
 	private static final String GROUP = "group";
@@ -169,6 +170,7 @@ public class UserRoleDaoImpl extends BaseDAO implements UserRoleDao {
 	private Document roleDocument() {
 		return new Document().append(ID, "$" + ID)
 				.append(DESCRIPTION, "$" + DESCRIPTION)
+				.append(TYPE, "$" + TYPE)
 				.append(USERS_FIELD, "$" + USERS_FIELD)
 				.append(EXPLORATORY_SHAPES_FIELD, "$" + EXPLORATORY_SHAPES_FIELD)
 				.append(PAGES_FIELD, "$" + PAGES_FIELD)
diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/dto/UserRoleDto.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/dto/UserRoleDto.java
index 21ce26d..b1d3337 100644
--- a/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/dto/UserRoleDto.java
+++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/dto/UserRoleDto.java
@@ -31,10 +31,10 @@ import java.util.Set;
 @ToString
 @JsonIgnoreProperties(ignoreUnknown = true)
 public class UserRoleDto {
-
 	@JsonProperty("_id")
 	private String id;
 	private String description;
+	private Type type;
 	private Set<String> pages;
 	private Set<String> computationals;
 	private Set<String> exploratories;
@@ -42,4 +42,12 @@ public class UserRoleDto {
 	private Set<String> exploratoryShapes;
 	private Set<String> groups;
 
+	private enum Type {
+		NOTEBOOK,
+		COMPUTATIONAL,
+		NOTEBOOK_SHAPE,
+		COMPUTATIONAL_SHAPE,
+		BILLING,
+		ADMINISTRATION
+	}
 }
diff --git a/services/self-service/src/main/resources/mongo/gcp/mongo_roles.json b/services/self-service/src/main/resources/mongo/gcp/mongo_roles.json
index 43d12e3..d3c22ba 100644
--- a/services/self-service/src/main/resources/mongo/gcp/mongo_roles.json
+++ b/services/self-service/src/main/resources/mongo/gcp/mongo_roles.json
@@ -2,6 +2,7 @@
   {
     "_id": "nbShapes_n1-highcpu-2_fetching",
     "description": "Use n1-highcpu-2 instance shape for notebook",
+    "type": "NOTEBOOK_SHAPE",
     "exploratory_shapes": [
       "n1-highcpu-2"
     ],
@@ -12,6 +13,7 @@
   {
     "_id": "nbShapes_n1-highcpu-8_fetching",
     "description": "Use n1-highcpu-8 instance shape for notebook",
+    "type": "NOTEBOOK_SHAPE",
     "exploratory_shapes": [
       "n1-highcpu-8"
     ],
@@ -22,6 +24,7 @@
   {
     "_id": "nbShapes_n1-highcpu-32_fetching",
     "description": "Use n1-highcpu-32 instance shape for notebook",
+    "type": "NOTEBOOK_SHAPE",
     "exploratory_shapes": [
       "n1-highcpu-32"
     ],
@@ -32,6 +35,7 @@
   {
     "_id": "nbShapes_n1-highmem-4_fetching",
     "description": "Use n1-highmem-4 instance shape for notebook",
+    "type": "NOTEBOOK_SHAPE",
     "exploratory_shapes": [
       "n1-highmem-4"
     ],
@@ -42,6 +46,7 @@
   {
     "_id": "nbShapes_n1-highmem-16_fetching",
     "description": "Use n1-highmem-16 instance shape for notebook",
+    "type": "NOTEBOOK_SHAPE",
     "exploratory_shapes": [
       "n1-highmem-16"
     ],
@@ -52,6 +57,7 @@
   {
     "_id": "nbShapes_n1-highmem-32_fetching",
     "description": "Use n1-highmem-32 instance shape for notebook",
+    "type": "NOTEBOOK_SHAPE",
     "exploratory_shapes": [
       "n1-highmem-32"
     ],
@@ -62,6 +68,7 @@
   {
     "_id": "nbShapes_n1-standard-2_fetching",
     "description": "Use n1-standard-2 instance shape for notebook",
+    "type": "NOTEBOOK_SHAPE",
     "exploratory_shapes": [
       "n1-standard-2"
     ],
@@ -72,6 +79,7 @@
   {
     "_id": "nbCreateDeeplearning",
     "description": "Create Notebook Deep Learning",
+    "type": "NOTEBOOK",
     "exploratories": [
       "docker.dlab-deeplearning"
     ],
@@ -82,6 +90,7 @@
   {
     "_id": "nbCreateJupyter",
     "description": "Create Notebook Jupyter",
+    "type": "NOTEBOOK",
     "exploratories": [
       "docker.dlab-jupyter"
     ],
@@ -92,6 +101,7 @@
   {
     "_id": "nbCreateJupyterLab",
     "description": "Create Notebook JupyterLab",
+    "type": "NOTEBOOK",
     "exploratories": [
       "docker.dlab-jupyterlab"
     ],
@@ -102,6 +112,7 @@
   {
     "_id": "nbCreateSuperset",
     "description": "Create Notebook Superset",
+    "type": "NOTEBOOK",
     "exploratories": [
       "docker.dlab-superset"
     ],
@@ -112,6 +123,7 @@
   {
     "_id": "nbCreateRstudio",
     "description": "Create Notebook RStudio",
+    "type": "NOTEBOOK",
     "exploratories": [
       "docker.dlab-rstudio"
     ],
@@ -122,6 +134,7 @@
   {
     "_id": "nbCreateTensor",
     "description": "Create Notebook Jupyter with TensorFlow",
+    "type": "NOTEBOOK",
     "exploratories": [
       "docker.dlab-tensor"
     ],
@@ -132,6 +145,7 @@
   {
     "_id": "nbCreateTensorRstudio",
     "description": "Create Notebook RStudio with TensorFlow",
+    "type": "NOTEBOOK",
     "exploratories": [
       "docker.dlab-tensor-rstudio"
     ],
@@ -142,6 +156,7 @@
   {
     "_id": "nbCreateZeppelin",
     "description": "Create Notebook Apache Zeppelin",
+    "type": "NOTEBOOK",
     "exploratories": [
       "docker.dlab-zeppelin"
     ],
@@ -152,6 +167,7 @@
   {
     "_id": "nbCreateDataEngine",
     "description": "Create Data Engine",
+    "type": "COMPUTATIONAL",
     "computationals": [
       "docker.dlab-dataengine"
     ],
@@ -162,6 +178,7 @@
   {
     "_id": "nbCreateDataEngineService",
     "description": "Create Data Engine Service",
+    "type": "COMPUTATIONAL",
     "computationals": [
       "docker.dlab-dataengine-service"
     ],
@@ -172,6 +189,7 @@
   {
     "_id": "compShapes_n1-standard-2_fetching",
     "description": "Use n1-standard-2 instance shape for cluster",
+    "type": "COMPUTATIONAL_SHAPE",
     "computational_shapes": [
       "n1-standard-2"
     ],
@@ -182,6 +200,7 @@
   {
     "_id": "compShapes_n1-highmem-4_fetching",
     "description": "Use n1-highmem-4 instance shape for cluster",
+    "type": "COMPUTATIONAL_SHAPE",
     "computational_shapes": [
       "n1-highmem-4"
     ],
@@ -192,6 +211,7 @@
   {
     "_id": "compShapes_n1-highmem-16_fetching",
     "description": "Use n1-highmem-16 instance shape for cluster",
+    "type": "COMPUTATIONAL_SHAPE",
     "computational_shapes": [
       "n1-highmem-16"
     ],
@@ -202,6 +222,7 @@
   {
     "_id": "compShapes_n1-highmem-32_fetching",
     "description": "Use n1-highmem-32 instance shape for cluster",
+    "type": "COMPUTATIONAL_SHAPE",
     "computational_shapes": [
       "n1-highmem-32"
     ],
@@ -212,6 +233,7 @@
   {
     "_id": "compShapes_n1-highcpu-8_fetching",
     "description": "Use n1-highcpu-8 instance shape for cluster",
+    "type": "COMPUTATIONAL_SHAPE",
     "computational_shapes": [
       "n1-highcpu-8"
     ],
@@ -222,6 +244,7 @@
   {
     "_id": "compShapes_n1-highcpu-2_fetching",
     "description": "Use n1-highcpu-2 instance shape for cluster",
+    "type": "COMPUTATIONAL_SHAPE",
     "computational_shapes": [
       "n1-highcpu-2"
     ],
@@ -232,6 +255,7 @@
   {
     "_id": "compShapes_n1-highcpu-32_fetching",
     "description": "Use n1-highcpu-32 instance shape for cluster",
+    "type": "COMPUTATIONAL_SHAPE",
     "computational_shapes": [
       "n1-highcpu-32"
     ],
@@ -242,6 +266,7 @@
   {
     "_id": "nbBillingReportFull",
     "description": "View full billing report for all users",
+    "type": "BILLING",
     "pages": [
       "/api/infrastructure_provision/billing"
     ],
@@ -252,6 +277,7 @@
   {
     "_id": "admin",
     "description": "Allow to execute administration operation",
+    "type": "ADMINISTRATION",
     "pages": [
       "environment/*",
       "/api/infrastructure/backup",
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/roles/roles.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/roles/roles.component.html
index af111c2..0a7c8e2 100644
--- a/services/self-service/src/main/resources/webapp/src/app/administration/roles/roles.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/roles/roles.component.html
@@ -45,6 +45,7 @@
               class="material-icons">keyboard_arrow_right</i></button>
         </div>
       </mat-step>
+
       <mat-step [completed]='false'>
         <ng-template matStepLabel>Users</ng-template>
         <div class="inner-step mat-reset">
@@ -58,35 +59,17 @@
               class="material-icons">keyboard_arrow_right</i></button>
         </div>
       </mat-step>
+
       <mat-step [completed]='false'>
         <ng-template matStepLabel>Roles</ng-template>
         <div class="inner-step mat-reset roles">
           <div class="selector-wrapper">
-            <mat-form-field>
-              <mat-select
-                multiple [compareWith]="compareObjects"
-                name="roles"
-                [(value)]="setupRoles"
-                disableOptionCentering
-                placeholder="Select roles"
-                panelClass="select-role"
-              >
-                <mat-option class="multiple-select" disabled>
-                  <a class="select ani" (click)="selectAllOptions(setupRoles, rolesList)">
-                    <i class="material-icons">playlist_add_check</i>&nbsp;All
-                  </a>
-                  <a class="deselect ani" (click)="selectAllOptions(setupRoles)">
-                    <i class="material-icons">clear</i>&nbsp;None
-                  </a>
-                </mat-option>
-                <mat-option *ngFor="let role of rolesList" [value]="role">
-                  {{ role }}
-                </mat-option>
-              </mat-select>
-              <button class="caret">
-                <i class="material-icons">keyboard_arrow_down</i>
-              </button>
-            </mat-form-field>
+            <multi-level-select-dropdown
+              (selectionChange)="onUpdate($event)"
+              name="roles"
+              [items]="rolesList"
+              [model]="setupRoles">
+            </multi-level-select-dropdown>
           </div>
         </div>
         <div class="text-center m-bott-10">
@@ -94,9 +77,10 @@
               class="material-icons">keyboard_arrow_left</i>Back</button>
           <button mat-raised-button (click)="resetDialog()" class="butt">Cancel</button>
           <button mat-raised-button (click)="manageAction('create', 'group')" class="butt butt-success"
-            [disabled]="!setupGroup || setupGroupName.errors?.patterns || setupGroupName.errors?.duplicate || !setupRoles.length > 0">Create</button>
+            [disabled]="!setupGroup || setupGroupName.errors?.patterns || setupGroupName.errors?.duplicate || !setupRoles.length">Create</button>
         </div>
       </mat-step>
+
     </mat-horizontal-stepper>
   </mat-card>
   <mat-divider></mat-divider>
@@ -112,27 +96,13 @@
         <th mat-header-cell *matHeaderCellDef class="roles"> Roles </th>
         <td mat-cell *matCellDef="let element" class="roles">
           <div class="inner-step mat-reset">
-            <div class="selector-wrapper-edit">
-              <mat-form-field class="select">
-                <mat-select multiple [compareWith]="compareObjects" name="selected_roles" disableOptionCentering
-                  [(value)]="element.selected_roles" placeholder="Select roles" class="roles-select" panelClass="select-role">
-                  <mat-option class="multiple-select" disabled>
-                    <a class="select ani" (click)="selectAllOptions(element, rolesList, 'selected_roles')">
-                      <i class="material-icons">playlist_add_check</i>&nbsp;All
-                    </a>
-                    <a class="deselect ani" (click)="selectAllOptions(element, null, 'selected_roles')">
-                      <i class="material-icons">clear</i>&nbsp;None
-                    </a>
-                  </mat-option>
-                  <mat-option *ngFor="let role of rolesList" [value]="role">
-                    {{ role }}
-                  </mat-option>
-                </mat-select>
-                <button class="caret">
-                  <i class="material-icons">keyboard_arrow_down</i>
-                </button>
-              </mat-form-field>
-            </div>
+              <multi-level-select-dropdown
+                (selectionChange)="onUpdate($event)"
+                [type]="element.group"
+                [items]="rolesList"
+                [model]="element.selected_roles">
+
+              </multi-level-select-dropdown>
           </div>
         </td>
       </ng-container>
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/roles/roles.component.scss b/services/self-service/src/main/resources/webapp/src/app/administration/roles/roles.component.scss
index dd14655..f2c10d8 100644
--- a/services/self-service/src/main/resources/webapp/src/app/administration/roles/roles.component.scss
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/roles/roles.component.scss
@@ -88,17 +88,23 @@
   }
 }
 
+.mat-horizontal-content-container{
+  overflow: visible !important;
+}
+
 .selector-wrapper {
   display: flex;
   align-self: center;
   width: 490px;
   height: 36px;
-  padding-left: 10px;
+  padding-left: 0;
   font-family: 'Open Sans', sans-serif;
   font-size: 15px;
   font-weight: 300;
   box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12);
-
+  multi-level-select-dropdown{
+    width: 100%;
+  }
   mat-form-field {
     width: 100%;
 
@@ -137,7 +143,6 @@
 }
 
 .roles {
-  // width: 30%;
 
   .selector-wrapper-edit {
     position: relative;
@@ -343,11 +348,11 @@ table {
   }
 
   .roles {
-    width: 30%;
+    width: 35%;
   }
 
   .users {
-    width: 40%;
+    width: 35%;
   }
 
   .actions {
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/roles/roles.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/roles/roles.component.ts
index 8afec35..01ff281 100644
--- a/services/self-service/src/main/resources/webapp/src/app/administration/roles/roles.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/roles/roles.component.ts
@@ -28,6 +28,7 @@ import { DICTIONARY } from '../../../dictionary/global.dictionary';
 import {ProgressBarService} from '../../core/services/progress-bar.service';
 import {ConfirmationDialogComponent, ConfirmationDialogType} from '../../shared/modal-dialog/confirmation-dialog';
 
+
 @Component({
   selector: 'dlab-roles',
   templateUrl: './roles.component.html',
@@ -38,11 +39,11 @@ export class RolesComponent implements OnInit {
 
   public groupsData: Array<any> = [];
   public roles: Array<any> = [];
-  public rolesList: Array<string> = [];
+  public rolesList: Array<any> = [];
   public setupGroup: string = '';
   public setupUser: string = '';
   public manageUser: string = '';
-  public setupRoles: Array<string> = [];
+  public setupRoles: Array<any> = [];
   public updatedRoles: Array<string> = [];
   public healthStatus: any;
   public delimitersRegex = /[-_]?/g;
@@ -72,7 +73,10 @@ export class RolesComponent implements OnInit {
       this.rolesService.getRolesData().subscribe(
         (roles: any) => {
           this.roles = roles;
-          this.rolesList = roles.map(role => role.description);
+          this.rolesList = roles.map((role, index) => {
+              return {role: role.description, type: role.type};
+          });
+
           this.updateGroupData(groups);
           this.getGroupsListCopy();
           this.stepperView = false;
@@ -103,7 +107,7 @@ export class RolesComponent implements OnInit {
           action, type, value: {
             name: this.setupGroup,
             users: this.setupUser ? this.setupUser.split(',').map(elem => elem.trim()) : [],
-            roleIds: this.extractIds(this.roles, this.setupRoles)
+            roleIds: this.extractIds(this.roles, this.setupRoles.map(v => v.role))
           }
         });
       this.stepperView = false;
@@ -126,7 +130,7 @@ export class RolesComponent implements OnInit {
       this.manageRolesGroups({
         action, type, value: {
           name: item.group,
-          roleIds: this.extractIds(this.roles, item.selected_roles),
+          roleIds: this.extractIds(this.roles, item.selected_roles.map(v => v.role)),
           users: item.users || []
         }
       });
@@ -202,7 +206,7 @@ export class RolesComponent implements OnInit {
   public updateGroupData(groups) {
     this.groupsData = groups.map(v => v).sort((a, b) => (a.group > b.group) ? 1 : ((b.group > a.group) ? -1 : 0));
     this.groupsData.forEach(item => {
-      item.selected_roles = item.roles.map(role => role.description);
+      item.selected_roles = item.roles.map(role => ({role: role.description, type: role.type}));
     });
   }
 
@@ -224,10 +228,6 @@ export class RolesComponent implements OnInit {
     });
   }
 
-  public compareObjects(o1: any, o2: any): boolean {
-    return o1.toLowerCase() === o2.toLowerCase();
-  }
-
   public resetDialog() {
     this.stepperView = false;
     this.setupGroup = '';
@@ -251,6 +251,14 @@ export class RolesComponent implements OnInit {
     this.healthStatusService.getEnvironmentHealthStatus()
       .subscribe((result: any) => this.healthStatus = result);
   }
+
+  public onUpdate($event): void {
+   if ($event.type) {
+     this.groupsData.filter(group => group.group === $event.type)[0].selected_roles = $event.model;
+   } else {
+     this.setupRoles = $event.model;
+   }
+  }
 }
 
 
@@ -273,6 +281,7 @@ export class RolesComponent implements OnInit {
   `,
   styles: [`.group-name { max-width: 96%; display: inline-block; vertical-align: bottom; }`]
 })
+
 export class ConfirmDeleteUserAccountDialogComponent {
   constructor(
     public dialogRef: MatDialogRef<ConfirmDeleteUserAccountDialogComponent>,
diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/form-controls/index.ts b/services/self-service/src/main/resources/webapp/src/app/shared/form-controls/index.ts
index bac0dd6..4ea5a14 100644
--- a/services/self-service/src/main/resources/webapp/src/app/shared/form-controls/index.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/form-controls/index.ts
@@ -25,6 +25,7 @@ import { MultiSelectDropdownComponent } from './multi-select-dropdown/multi-sele
 import { DirectivesModule } from '../../core/directives';
 import { KeysPipeModule, UnderscorelessPipeModule } from '../../core/pipes';
 import { BubbleModule } from '..';
+import {MultiLevelSelectDropdownComponent} from './multi-level-select-dropdown/multi-level-select-dropdown.component';
 
 export * from './multi-select-dropdown/multi-select-dropdown.component';
 export * from './dropdown-list/dropdown-list.component';
@@ -37,7 +38,7 @@ export * from './dropdown-list/dropdown-list.component';
     UnderscorelessPipeModule,
     BubbleModule
   ],
-  declarations: [DropdownListComponent, MultiSelectDropdownComponent],
-  exports: [DropdownListComponent, MultiSelectDropdownComponent]
+  declarations: [DropdownListComponent, MultiSelectDropdownComponent, MultiLevelSelectDropdownComponent],
+  exports: [DropdownListComponent, MultiSelectDropdownComponent, MultiLevelSelectDropdownComponent]
 })
 export class FormControlsModule {}
diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/form-controls/multi-level-select-dropdown/multi-level-select-dropdown.component.html b/services/self-service/src/main/resources/webapp/src/app/shared/form-controls/multi-level-select-dropdown/multi-level-select-dropdown.component.html
new file mode 100644
index 0000000..f6b52da
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/form-controls/multi-level-select-dropdown/multi-level-select-dropdown.component.html
@@ -0,0 +1,70 @@
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one
+  ~ or more contributor license agreements.  See the NOTICE file
+  ~ distributed with this work for additional information
+  ~ regarding copyright ownership.  The ASF licenses this file
+  ~ to you 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.
+  -->
+
+<div class="dropdown-multiselect btn-group" ngClass="{{type || ''}}">
+  <button type="button" #list (click)="multiactions.toggle($event, list)">
+    <span class="ellipsis" *ngIf="model.length === 0">Select roles</span>
+    <span class="selected-items ellipsis" *ngIf="model.length !== 0">
+      {{selectedRolesList()}}
+    </span>
+    <span class="caret-btn"><i class="material-icons">keyboard_arrow_down</i></span>
+  </button>
+
+  <bubble-up #multiactions position="bottom" [keep-open]="true" class="mt-5">
+    <ul class="list-menu" id="scrolling">
+      <li class="filter-actions">
+        <a class="select_all" (click)="selectAllOptions($event)">
+          <i class="material-icons">playlist_add_check</i>&nbsp;All
+        </a>
+        <a class="deselect_all" (click)="deselectAllOptions($event)">
+          <i class="material-icons">clear</i>&nbsp;None
+        </a>
+      </li>
+
+        <ng-template  ngFor let-item [ngForOf]="items" let-i="index">
+          <li class="role-label" role="presentation" *ngIf="i === 0 || model && item.type !== items[i - 1].type" (click)="toggleItemsForLable(item.type, $event)">
+            <a href="#" class="list-item" role="menuitem">
+              <span class="arrow" [ngClass]="{'rotate-arrow': isOpenCategory[item.type], 'arrow-checked': selectedAllInCattegory(item.type) || selectedSomeInCattegory(item.type)}"></span>
+              <span class="empty-checkbox" [ngClass]="{'checked': selectedAllInCattegory(item.type) || selectedSomeInCattegory(item.type)}" (click)="toggleselectedCategory($event, model, item.type);$event.stopPropagation()" >
+                <span class="checked-checkbox" *ngIf="selectedAllInCattegory(item.type)"></span>
+                <span class="line-checkbox" *ngIf="selectedSomeInCattegory(item.type)"></span>
+              </span>
+              {{labels[item.type] || item.type | titlecase}}
+            </a>
+          </li>
+
+          <li class="role-item" role="presentation" *ngIf="model && isOpenCategory[item.type]" >
+            <a href="#" class="list-item" role="menuitem" (click)="toggleSelectedOptions($event, model, item)">
+              <span class="empty-checkbox" [ngClass]="{'checked': checkInModel(item.role)}">
+                <span class="checked-checkbox" *ngIf="checkInModel(item.role)"></span>
+              </span>
+              {{item.role}}
+            </a>
+          </li>
+        </ng-template>
+
+      <li *ngIf="items?.length == 0">
+        <a role="menuitem" class="list-item">
+          <span class="material-icons">visibility_off</span>
+          No {{type}}
+        </a>
+      </li>
+    </ul>
+  </bubble-up>
+</div>
diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/form-controls/multi-level-select-dropdown/multi-level-select-dropdown.component.scss b/services/self-service/src/main/resources/webapp/src/app/shared/form-controls/multi-level-select-dropdown/multi-level-select-dropdown.component.scss
new file mode 100644
index 0000000..00d9a80
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/form-controls/multi-level-select-dropdown/multi-level-select-dropdown.component.scss
@@ -0,0 +1,319 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+.dropdown-list,
+.dropdown-multiselect {
+  width: 100%;
+  position: relative;
+}
+
+.dropdown-list button,
+.dropdown-multiselect button {
+  height: 38px;
+  width: 100%;
+  background: #fff;
+  padding-left: 15px;
+  font-size: 14px;
+  // height: 34px;
+  text-align: left;
+  white-space: nowrap;
+  cursor: pointer;
+  border-radius: 0;
+  border: none;
+  outline: none;
+  box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .2), 0 2px 2px 0 rgba(0, 0, 0, .14), 0 1px 5px 0 rgba(0, 0, 0, .12);
+}
+
+.dropdown-list {
+  button {
+    line-height: 38px;
+
+    span {
+      color: #4a5c89;
+
+      em {
+        font-size: 13px;
+        color: #35afd5;
+        margin-right: 0px;
+        font-style: normal;
+      }
+    }
+  }
+}
+
+.dropdown-list button:active,
+.dropdown-list button:focus,
+.dropdown-multiselect button:active,
+.dropdown-multiselect button:focus {
+  box-shadow: 0 5px 5px -3px rgba(0, 0, 0, .2), 0 8px 10px 1px rgba(0, 0, 0, .14), 0 3px 14px 2px rgba(0, 0, 0, .12);
+}
+
+.dropdown-multiselect {
+  button {
+    span {
+      color: #999;
+      font-weight: 300;
+      display: inline-block;
+      max-width: 80%;
+    }
+
+    .selected-items {
+      color: #4a5c89;
+      max-width: 477px;
+    }
+  }
+}
+
+.selected-items strong {
+  font-weight: 300;
+}
+
+.dropdown-list,
+.dropdown-multiselect {
+  .caret-btn {
+    position: absolute;
+    top: 0;
+    right: 0;
+    width: 40px;
+    height: 100%;
+    text-align: center;
+    padding: 7px;
+    -webkit-appearance: none;
+    -moz-appearance: none;
+    border-left: 1px solid #ececec;
+    background: #fff;
+    color: #36afd5 !important;
+  }
+
+  .list-menu {
+    width: 100%;
+    max-height: 450px;
+    left: 0;
+    padding: 0;
+    margin: 0;
+    overflow-y: auto;
+    overflow-x: hidden;
+
+    li {
+      padding: 0;
+      margin: 0;
+    }
+    .role-item{
+      padding-left: 30px;
+    }
+
+
+  }
+
+  &.statuses {
+    .list-menu {
+      .list-item {
+        text-transform: capitalize;
+      }
+    }
+  }
+
+  &.resources {
+    .list-menu {
+      .list-item {
+        text-transform: capitalize;
+      }
+    }
+  }
+}
+
+.dropdown-list .list-menu a,
+.dropdown-multiselect .list-menu li a {
+  display: block;
+  padding: 10px;
+  padding-left: 15px;
+  position: relative;
+  font-weight: 300;
+  cursor: pointer;
+  color: #4a5c89;
+  text-decoration: none;
+}
+
+.dropdown-multiselect .list-menu li a {
+  padding-left: 45px;
+  transition: all .45s ease-in-out;
+}
+
+.dropdown-list .list-menu a:hover,
+.dropdown-multiselect .list-menu a:hover {
+  background: #f7f7f7;
+  color: #35afd5;
+}
+
+.dropdown-multiselect .list-menu .filter-actions {
+  display: flex;
+  cursor: pointer;
+  border-bottom: 1px solid #ececec;
+}
+
+.dropdown-multiselect .list-menu .filter-actions a {
+  width: 50%;
+  color: #35afd5;
+  display: block;
+  padding: 0;
+  line-height: 40px !important;
+  text-align: center;
+}
+
+.dropdown-list {
+
+  .list-menu,
+  .title {
+    span {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      font-weight: 300;
+    }
+  }
+}
+
+.dropdown-list .list-menu li span.caption {
+  display: block;
+  padding: 10px 15px;
+  cursor: default;
+}
+
+.dropdown-list .list-menu li i,
+.dropdown-list .list-menu li strong {
+  display: inline-block;
+  width: 30px;
+  text-align: center;
+  vertical-align: middle;
+  color: #35afd5;
+  line-height: 26px;
+}
+
+.dropdown-list .list-menu li i {
+  vertical-align: sub;
+  font-size: 18px;
+}
+
+.dropdown-list .list-menu a {
+  padding: 12px;
+  padding-left: 15px;
+  position: relative;
+  font-weight: 300;
+  cursor: pointer;
+
+  em {
+    font-size: 13px;
+    color: #35afd5;
+    margin-right: 0px;
+    font-style: normal;
+  }
+}
+
+.dropdown-list .list-menu a.empty {
+  height: 36px;
+}
+
+.dropdown-multiselect .list-menu .filter-actions i {
+  vertical-align: sub;
+  color: #35afd5;
+  font-size: 18px;
+  line-height: 26px;
+  transition: all .45s ease-in-out;
+}
+
+.dropdown-multiselect .list-menu .select_all:hover,
+.dropdown-multiselect .list-menu .select_all:hover i {
+  color: #4eaf3e !important;
+  background: #f9fafb;
+}
+
+.dropdown-multiselect .list-menu .deselect_all:hover,
+.dropdown-multiselect .list-menu .deselect_all:hover i {
+  color: #f1696e !important;
+  background: #f9fafb;
+}
+
+.dropdown-multiselect .list-menu a {
+  span {
+    position: absolute;
+    top: 10px;
+    left: 25px;
+    color: #35afd5;
+
+    &.checked-checkbox {
+      top: 0px;
+      left: 4px;
+      width: 5px;
+      height: 10px;
+      border-bottom: 2px solid white;
+      border-right: 2px solid white;
+      position: absolute;
+      transform: rotate(45deg);
+    }
+
+    &.line-checkbox {
+      top: 0px;
+      left: 2px;
+      width: 8px;
+      height: 7px;
+      border-bottom: 2px solid white;
+    }
+
+    &.arrow{
+      width: 16px;
+      height: 14px;
+      border: 8px solid transparent;
+      border-left: 8px solid lightgrey;
+      left: 10px;
+      top: 12px;
+      border-radius: 3px;
+
+      &.rotate-arrow{
+        transform: rotate(90deg);
+        transition: .1s ease-in-out;
+        top: 15px;
+        left: 6px;
+      }
+
+      &.arrow-checked{
+        border-left: 8px solid #35afd5;
+      }
+    }
+  }
+
+
+}
+
+.dropdown-multiselect.btn-group.open .dropdown-toggle {
+  box-shadow: none;
+}
+
+.empty-checkbox {
+  width: 16px;
+  height: 16px;
+  border-radius: 2px;
+  border: 2px solid lightgrey;
+  margin-top: 2px;
+  position: relative;
+  &.checked {
+    border-color: #35afd5;
+    background-color: #35afd5;
+  }
+}
+
diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/form-controls/multi-level-select-dropdown/multi-level-select-dropdown.component.ts b/services/self-service/src/main/resources/webapp/src/app/shared/form-controls/multi-level-select-dropdown/multi-level-select-dropdown.component.ts
new file mode 100644
index 0000000..fe8b36a
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/form-controls/multi-level-select-dropdown/multi-level-select-dropdown.component.ts
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import { Input, Output, Component, EventEmitter } from '@angular/core';
+
+@Component({
+  selector: 'multi-level-select-dropdown',
+  templateUrl: 'multi-level-select-dropdown.component.html',
+  styleUrls: ['multi-level-select-dropdown.component.scss']
+})
+
+export class MultiLevelSelectDropdownComponent {
+
+  @Input() items: Array<any>;
+  @Input() model: Array<any>;
+  @Input() type: string;
+  @Output() selectionChange: EventEmitter<{}> = new EventEmitter();
+
+  public isOpenCategory = {
+  };
+
+  public labels = {
+    COMPUTATIONAL_SHAPE: 'Compute shapes',
+    NOTEBOOK_SHAPE: 'Notebook shape'
+  };
+
+  toggleSelectedOptions($event, model, value) {
+    $event.preventDefault();
+    const currRole = model.filter(v => v.role === value.role).length;
+    currRole ? this.model = model.filter(v => v.role !== value.role) : model.push(value);
+    this.onUpdate($event);
+  }
+
+  toggleselectedCategory($event, model, value) {
+    $event.preventDefault();
+    const categoryItems = this.items.filter(role => role.type === value);
+    this.selectedAllInCattegory(value) ? this.model = this.model.filter(role => role.type !== value) : categoryItems.forEach(role => {
+      if (!model.filter(mod => mod.role === role.role).length) {this.model.push(role); }
+    });
+    this.onUpdate($event);
+  }
+
+  selectAllOptions($event) {
+
+    $event.preventDefault();
+    this.model = [...this.items];
+    this.onUpdate($event);
+    $event.preventDefault();
+  }
+
+  deselectAllOptions($event) {
+    this.model = [];
+    this.onUpdate($event);
+    $event.preventDefault();
+  }
+
+  onUpdate($event): void {
+    this.selectionChange.emit({ model: this.model, type: this.type, $event });
+  }
+
+  public toggleItemsForLable(label, $event) {
+    this.isOpenCategory[label] = !this.isOpenCategory[label];
+    $event.preventDefault();
+  }
+
+  public selectedAllInCattegory(category) {
+    const selected = this.model.filter(role => role.type === category);
+    const categoryItems = this.items.filter(role => role.type === category);
+    return selected.length === categoryItems.length;
+  }
+
+  public selectedSomeInCattegory(category) {
+    const selected = this.model.filter(role => role.type === category);
+    const categoryItems = this.items.filter(role => role.type === category);
+    return selected.length && selected.length !== categoryItems.length;
+  }
+
+  public checkInModel(item) {
+    return this.model.filter(v => v.role === item).length;
+  }
+
+  public selectedRolesList() {
+    return this.model.map(role => role.role).join(',');
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/assets/styles/_theme.scss b/services/self-service/src/main/resources/webapp/src/assets/styles/_theme.scss
index 0353978..7b48bba 100644
--- a/services/self-service/src/main/resources/webapp/src/assets/styles/_theme.scss
+++ b/services/self-service/src/main/resources/webapp/src/assets/styles/_theme.scss
@@ -656,3 +656,9 @@ mat-progress-bar {
     background-color: #baf0f7;
   }
 }
+.manage-roles{
+  .mat-horizontal-content-container{
+    overflow: visible !important;
+  }
+}
+


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@dlab.apache.org
For additional commands, e-mail: commits-help@dlab.apache.org