You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dlab.apache.org by an...@apache.org on 2019/05/20 11:45:57 UTC

[incubator-dlab] 01/09: added administration as a top level on structure

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

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

commit eeb4793a52a2d7970654734528c6e1955467017b
Author: Andriana Kovalyshyn <An...@epam.com>
AuthorDate: Mon May 20 13:02:34 2019 +0300

    added administration as a top level on structure
---
 .../app/administration/administration.module.ts    |  31 +++
 .../backup-dilog/backup-dilog.component.html       |  82 ++++++
 .../backup-dilog/backup-dilog.component.scss       |  26 ++
 .../backup-dilog/backup-dilog.component.ts         |  75 ++++++
 .../src/app/administration/management/index.ts     |  82 ++++++
 .../manage-environment-dilog.component.html        |  73 ++++++
 .../manage-environment-dilog.component.scss        |  87 ++++++
 .../manage-environment-dilog.component.ts          | 129 +++++++++
 .../group-name-validarion.directive.ts             |  37 +++
 .../manage-roles-groups.component.html             | 154 +++++++++++
 .../manage-roles-groups.component.scss             | 292 +++++++++++++++++++++
 .../manage-roles-groups.component.ts               | 192 ++++++++++++++
 .../management-grid/management-grid.component.html | 161 ++++++++++++
 .../management-grid/management-grid.component.scss |  40 +++
 .../management-grid/management-grid.component.ts   | 172 ++++++++++++
 .../management/management.component.html           |  55 ++++
 .../management/management.component.scss           |  37 +++
 .../management/management.component.ts             | 275 +++++++++++++++++++
 .../administration/management/management.model.ts  |  58 ++++
 .../ssn-monitor/ssn-monitor.component.html         | 117 +++++++++
 .../ssn-monitor/ssn-monitor.component.scss         |  50 ++++
 .../ssn-monitor/ssn-monitor.component.ts           |  59 +++++
 .../webapp/src/app/administration/project/index.ts |  39 +++
 .../administration/project/project.component.html  |  19 ++
 .../administration/project/project.component.scss  |  18 ++
 .../administration/project/project.component.ts    |  34 +++
 26 files changed, 2394 insertions(+)

diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/administration.module.ts b/services/self-service/src/main/resources/webapp/src/app/administration/administration.module.ts
new file mode 100644
index 0000000..2ae70a4
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/administration.module.ts
@@ -0,0 +1,31 @@
+/*
+ * 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 { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+import { ManagenementModule } from './management';
+import { ProjectModule } from './project';
+
+@NgModule({
+  imports: [CommonModule, ManagenementModule, ProjectModule],
+  declarations: [],
+  exports: [ManagenementModule, ProjectModule]
+})
+export class AdministrationModule { }
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/backup-dilog/backup-dilog.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/management/backup-dilog/backup-dilog.component.html
new file mode 100644
index 0000000..9c797ad
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/backup-dilog/backup-dilog.component.html
@@ -0,0 +1,82 @@
+<!--
+  ~ 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.
+  -->
+
+<modal-dialog #bindDialog modalClass="backup-dialog modal-sm">
+  <modal-header>
+    <h4 class="modal-title">Backup options</h4>
+  </modal-header>
+  <modal-content>
+    <div class="content-box" *ngIf="backupOptions">
+      <div class="hold-block">
+        <mat-slide-toggle
+          labelPosition="before"
+          [checked]="backupOptions.configFiles[0] === 'all'"
+          (change)="onHoldChanged($event, 'configFiles')">
+          <span class="hold-label">Configuration files</span>
+        </mat-slide-toggle>
+      </div>
+      <div class="hold-block">
+        <mat-slide-toggle
+          labelPosition="before"
+          [checked]="backupOptions.keys[0] === 'all'"
+          (change)="onHoldChanged($event, 'keys')">
+          <span class="hold-label">User keys</span>
+        </mat-slide-toggle>
+      </div>
+      <div class="hold-block">
+        <mat-slide-toggle
+          labelPosition="before"
+          [checked]="backupOptions.databaseBackup"
+          (change)="onHoldChanged($event, 'databaseBackup')">
+          <span class="hold-label">Database</span>
+        </mat-slide-toggle>
+      </div>
+      <div class="hold-block">
+        <mat-slide-toggle
+          labelPosition="before"
+          [checked]="backupOptions.certificates[0] === 'all'"
+          (change)="onHoldChanged($event, 'certificates')">
+          <span class="hold-label">SSL Certificates</span>
+        </mat-slide-toggle>
+      </div>
+      <div class="hold-block">
+        <mat-slide-toggle
+          labelPosition="before"
+          [checked]="backupOptions.jars[0] === 'all'"
+          (change)="onHoldChanged($event, 'jars')">
+          <span class="hold-label">JAR files</span>
+        </mat-slide-toggle>
+      </div>
+      <div class="hold-block">
+        <mat-slide-toggle
+          labelPosition="before"
+          [checked]="backupOptions.logsBackup"
+          (change)="onHoldChanged($event, 'logsBackup')">
+          <span class="hold-label">Log files</span>
+        </mat-slide-toggle>
+      </div>
+      <div class="text-center m-top-30 m-bott-10">
+        <button mat-raised-button type="button" class="butt" (click)="bindDialog.close(); backupOptions.setDegault()">
+          Cancel
+        </button>
+        <button mat-raised-button type="button" (click)="applyOptions()" class="butt butt-success" [disabled]="!valid">Apply</button>
+      </div>
+    </div>
+  </modal-content>
+</modal-dialog>
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/backup-dilog/backup-dilog.component.scss b/services/self-service/src/main/resources/webapp/src/app/administration/management/backup-dilog/backup-dilog.component.scss
new file mode 100644
index 0000000..e27a7a4
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/backup-dilog/backup-dilog.component.scss
@@ -0,0 +1,26 @@
+/*!
+ * 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.
+ */
+
+.hold-block {
+  padding: 5px 10px;
+  transition: all 0.4s ease-out;
+  &:hover {
+    color: #35afd5;
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/backup-dilog/backup-dilog.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/management/backup-dilog/backup-dilog.component.ts
new file mode 100644
index 0000000..8b8ed61
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/backup-dilog/backup-dilog.component.ts
@@ -0,0 +1,75 @@
+/*
+ * 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 { Component, OnInit, ViewChild, Output, EventEmitter } from '@angular/core';
+import { DICTIONARY } from '../../../../dictionary/global.dictionary';
+
+import { BackupOptionsModel } from '../management.model';
+
+@Component({
+  selector: 'dlab-backup-dilog',
+  templateUrl: './backup-dilog.component.html',
+  styleUrls: ['./backup-dilog.component.scss']
+})
+export class BackupDilogComponent implements OnInit {
+  readonly DICTIONARY = DICTIONARY;
+  public backupOptions: BackupOptionsModel = new BackupOptionsModel([], [], [], [], false, false);
+  public valid: boolean = true;
+
+  @ViewChild('bindDialog') bindDialog;
+  @Output() backupOpts: EventEmitter<{}> = new EventEmitter();
+
+  ngOnInit() {
+    this.backupOptions.setDegault();
+    this.bindDialog.onClosing = () => this.backupOptions.setDegault();
+  }
+
+  public open(param): void {
+    this.valid = true;
+    this.bindDialog.open(param);
+  }
+
+  public onHoldChanged($event, key): void {
+    this.backupOptions[key] instanceof Array
+      ? (this.backupOptions[key][0] = $event.checked ? 'all' : 'skip')
+      : (this.backupOptions[key] = !this.backupOptions[key]);
+
+    this.checkValidity();
+  }
+
+  public applyOptions(): void {
+    this.backupOpts.emit(this.backupOptions);
+    this.backupOptions.setDegault();
+    this.bindDialog.close();
+  }
+
+  private checkValidity(): void {
+    const items = [];
+
+    Object.keys(this.backupOptions).forEach(el => {
+      if (this.backupOptions[el] instanceof Array) {
+        if (this.backupOptions[el][0] && this.backupOptions[el][0] !== 'skip') items.push(this.backupOptions[el][0]);
+      } else {
+        if (this.backupOptions[el]) items.push(this.backupOptions[el]) ;
+      }
+    });
+
+    this.valid = items.length > 0;
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/index.ts b/services/self-service/src/main/resources/webapp/src/app/administration/management/index.ts
new file mode 100644
index 0000000..7d15b50
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/index.ts
@@ -0,0 +1,82 @@
+/*
+ * 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 { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+
+import {
+  ModalModule,
+  UploadKeyDialogModule,
+  ProgressDialogModule,
+  BubbleModule,
+  ConfirmationDialogModule
+} from '../../shared';
+import { MaterialModule } from '../../shared/material.module';
+
+import { ManagementComponent } from './management.component';
+import { ManagementGridComponent, ConfirmationDialogComponent } from './management-grid/management-grid.component';
+import { ComputationalResourcesModule } from '../../resources/computational/computational-resources-list';
+
+import { FormControlsModule } from '../../shared/form-controls';
+import { BackupDilogComponent } from './backup-dilog/backup-dilog.component';
+import { ManageEnvironmentComponent, ConfirmActionDialogComponent } from './manage-environment/manage-environment-dilog.component';
+
+import { GroupNameValidationDirective } from './manage-roles-groups/group-name-validarion.directive';
+import { DirectivesModule } from '../../core/directives';
+
+import { SsnMonitorComponent } from './ssn-monitor/ssn-monitor.component';
+import { ManageRolesGroupsComponent, ConfirmDeleteUserAccountDialogComponent } from './manage-roles-groups/manage-roles-groups.component';
+import { ProjectModule } from '../project';
+
+export * from './management.component';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    ReactiveFormsModule,
+    ProjectModule,
+    ModalModule,
+    UploadKeyDialogModule,
+    ProgressDialogModule,
+    BubbleModule,
+    ConfirmationDialogModule,
+    ComputationalResourcesModule,
+    FormControlsModule,
+    DirectivesModule,
+    MaterialModule
+  ],
+  declarations: [
+    ManagementComponent,
+    ManagementGridComponent,
+
+    GroupNameValidationDirective,
+    BackupDilogComponent,
+    ManageEnvironmentComponent,
+    ConfirmationDialogComponent,
+    ConfirmActionDialogComponent,
+    ConfirmDeleteUserAccountDialogComponent,
+    SsnMonitorComponent,
+    ManageRolesGroupsComponent
+  ],
+  entryComponents: [ConfirmationDialogComponent, ConfirmActionDialogComponent, ConfirmDeleteUserAccountDialogComponent],
+  exports: [ManagementComponent]
+})
+export class ManagenementModule {}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.html
new file mode 100644
index 0000000..094ec9b
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.html
@@ -0,0 +1,73 @@
+<!--
+  ~ 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.
+  -->
+
+<modal-dialog #bindDialog modalClass="manage-env-dialog modal-xl-s">
+  <modal-header>
+    <h4 class="modal-title">Manage environment</h4>
+  </modal-header>
+  <modal-content>
+    <div class="content-box">
+      <div *ngIf="usersList.length">
+        <form [formGroup]="manageUsersForm" (submit)="setBudgetLimits(manageUsersForm.value)" novalidate>
+          <mat-list>
+            <mat-list-item class="list-header">
+              <div class="username">User</div>
+              <div class="quotes" *ngIf="DICTIONARY.cloud_provider !== 'gcp'">Limit</div>
+              <div class="action">Actions</div>
+            </mat-list-item>
+            <div class="scrolling-content" id="scrolling" formArrayName="users">
+              <mat-list-item  *ngFor="let item of usersEnvironments.controls; let i=index" [formGroupName]="i" class="list-item"> 
+                <div class="username ellipsis">{{ manageUsersForm.controls['users'].controls[i].value['name'] }}</div>
+                <div class="quotes" *ngIf="DICTIONARY.cloud_provider !== 'gcp'">
+                    <input type="number" min="0" placeholder="Enter limit, in USD" formControlName="budget">
+                    <span class="danger_color" *ngIf="!manageUsersForm?.controls['users'].controls[i].controls['budget'].valid && !manageUsersForm?.controls['users'].controls[i].controls['budget'].hasError('overrun')">Only positive integers are allowed</span>
+                    <span class="danger_color" *ngIf="manageUsersForm?.controls['users'].controls[i].controls['budget'].hasError('overrun')">Per-user quotes cannot be greater than total budget</span>
+                </div>
+                <div class="action">
+                  <span *ngIf=" manageUsersForm.controls['users'].controls[i].value['status'] === 'ACTIVE'; else not_active" matTooltip="Stop" matTooltipPosition="above" (click)="applyAction('stop', item)"><i class="material-icons">pause_circle_outline</i></span>
+
+                  <ng-template #not_active>
+                    <span class="disabled" matTooltip="User's environment is not active" matTooltipPosition="above">
+                      <i class="material-icons">pause_circle_outline</i>
+                    </span>
+                  </ng-template>
+
+                  <span matTooltip="Terminate" matTooltipPosition="above" (click)="applyAction('terminate', item)"><i class="material-icons">phonelink_off</i></span>
+                </div>
+              </mat-list-item>
+            </div>
+            <div class="control-group total-budget" *ngIf="DICTIONARY.cloud_provider !== 'gcp'">
+              <label class="label">Total budget</label>
+              <div class="control">
+                <input type="number" formControlName="total" placeholder="Enter total budget, in USD">
+                <span class="danger_color" *ngIf="manageUsersForm?.controls['total'].hasError('overrun')">Total budget cannot be lower than a sum of users quotes</span>
+              </div>
+            </div>
+            <div class="text-center m-top-30" *ngIf="DICTIONARY.cloud_provider !== 'gcp'">
+              <button mat-raised-button type="button" (click)="bindDialog.close()" class="butt action">Cancel</button>
+              <button mat-raised-button type="submit" [disabled]="!manageUsersForm.valid"
+                      class="butt butt-success" [ngClass]="{'not-allowed': !manageUsersForm.valid}">Apply</button>
+            </div>
+          </mat-list>
+        </form>
+      </div>
+      <div class="info message" *ngIf="!usersList.length">No active users environments</div>
+    </div>
+  </modal-content>
+</modal-dialog>
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.scss b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.scss
new file mode 100644
index 0000000..b268e6f
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.scss
@@ -0,0 +1,87 @@
+/*!
+ * 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.
+ */
+
+.manage-env-dialog {
+  .mat-list {
+    .mat-list-item {
+      height: 60px;
+      position: relative;
+      .mat-list-item-content {
+        padding: 0 !important;
+        justify-content: space-between;
+        color: #577289;
+      }
+    }
+  }
+  .scrolling-content {
+    max-height: 300px;
+    min-height: 85px;
+    overflow-y: auto;
+  }
+  .username {
+    width: 45%;
+  }
+  .quotes {
+    width: 40%;
+    margin-right: 10px;
+    position: relative;
+    .danger_color {
+      position: absolute;
+      left: 0;
+      top: 36px;
+    }
+  }
+  .action {
+    width: 15%;
+    span {
+      padding: 3px;
+      cursor: pointer;
+      &:hover {
+        color: #35afd5;
+      }
+      i {
+        font-size: 20px;
+      }
+    }
+    .disabled {
+      cursor: not-allowed !important;
+      pointer-events: all;
+      opacity: .6;
+      &:hover {
+        color: #6b8299;
+      }
+    }
+  }
+  .total-budget {
+    border-top: 1px solid #edf1f5;
+    padding-top: 15px;
+    .control {
+      position: relative;
+      width: 56%;
+      .danger_color {
+        position: absolute;
+        left: 0;
+        bottom: -20px;
+      }
+    }
+    .label {
+      width: 44%;
+    }
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.ts
new file mode 100644
index 0000000..ae7d7d1
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.ts
@@ -0,0 +1,129 @@
+/*
+ * 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 { Component, ViewChild, Output, EventEmitter, ViewEncapsulation, Inject } from '@angular/core';
+import { Validators, FormBuilder, FormGroup, FormArray } from '@angular/forms';
+import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
+import { DICTIONARY } from '../../../../dictionary/global.dictionary';
+
+@Component({
+  selector: 'dlab-manage-env-dilog',
+  templateUrl: './manage-environment-dilog.component.html',
+  styleUrls: ['./manage-environment-dilog.component.scss'],
+  encapsulation: ViewEncapsulation.None
+})
+export class ManageEnvironmentComponent {
+  readonly DICTIONARY = DICTIONARY;
+  public usersList: Array<string> = [];
+  public manageUsersForm: FormGroup;
+  public manageTotalsForm: FormGroup;
+
+  @ViewChild('bindDialog') bindDialog;
+  @Output() manageEnv: EventEmitter<{}> = new EventEmitter();
+  @Output() setBudget: EventEmitter<{}> = new EventEmitter();
+
+  constructor(
+    private _fb: FormBuilder,
+    public dialog: MatDialog
+  ) { }
+
+  get usersEnvironments(): FormArray {
+    return <FormArray>this.manageUsersForm.get('users');
+  }
+
+  public open(param, data, settings): void {
+    this.usersList = data;
+    !this.manageUsersForm && this.initForm();
+
+    this.manageUsersForm.setControl('users',
+    this._fb.array((this.usersList || []).map((x: any) => this._fb.group({
+      name: x.name, budget: [x.budget, [Validators.min(0), this.userValidityCheck.bind(this)]], status: x.status
+    }))));
+    this.manageUsersForm.controls['total'].setValue(settings.conf_max_budget || null);
+    this.bindDialog.open(param);
+  }
+
+  public setBudgetLimits(value) {
+    this.setBudget.emit(value);
+    this.bindDialog.close();
+  }
+
+  public applyAction(action, user) {
+    const dialogRef: MatDialogRef<ConfirmActionDialogComponent> = this.dialog.open(
+      ConfirmActionDialogComponent, { data: {action, user: user.value.name}, width: '550px', panelClass: 'error-modalbox' });
+    dialogRef.afterClosed().subscribe(result => {
+      if (result) this.manageEnv.emit({action, user: user.value.name});
+    });
+  }
+
+  private initForm(): void {
+    this.manageUsersForm = this._fb.group({
+      total: [null, [Validators.min(0), this.totalValidityCheck.bind(this)]],
+      users: this._fb.array([this._fb.group({ name: '', budget: null, status: ''})])
+    });
+  }
+
+  private getCurrentUsersTotal(): number {
+    return this.manageUsersForm.value.users.reduce((memo, el) => memo += el.budget, 0);
+  }
+
+  private getCurrentTotalValue(): number {
+    return this.manageUsersForm.value.total;
+  }
+
+  private totalValidityCheck(control) {
+    return (control && control.value)
+      ? (control.value >= this.getCurrentUsersTotal() ? null : { overrun: true })
+      : null;
+  }
+
+  private userValidityCheck(control) {
+    if (control && control.value) {
+      return (this.getCurrentTotalValue() && this.getCurrentTotalValue() < this.getCurrentUsersTotal()) ? { overrun: true } : null;
+    }
+  }
+}
+
+@Component({
+  selector: 'dialog-result-example-dialog',
+  template: `
+  <div class="dialog-header">
+    <h4 class="modal-title"><span class="capitalize">{{ data.action }}</span> resource</h4>
+    <button type="button" class="close" (click)="dialogRef.close()">&times;</button>
+  </div>
+  <div mat-dialog-content class="content">
+    <p>Environment of <b>{{ data.user }}</b> will be
+      <span *ngIf="data.action === 'terminate'"> terminated.</span>
+      <span *ngIf="data.action === 'stop'">stopped.</span>
+    </p>
+    <p class="m-top-20"><strong>Do you want to proceed?</strong></p>
+  </div>
+  <div class="text-center">
+    <button type="button" class="butt" mat-raised-button (click)="dialogRef.close()">No</button>
+    <button type="button" class="butt butt-success" mat-raised-button (click)="dialogRef.close(true)">Yes</button>
+  </div>
+  `,
+  styles: []
+})
+export class ConfirmActionDialogComponent {
+  constructor(
+    public dialogRef: MatDialogRef<ConfirmActionDialogComponent>,
+    @Inject(MAT_DIALOG_DATA) public data: any
+  ) { }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/group-name-validarion.directive.ts b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/group-name-validarion.directive.ts
new file mode 100644
index 0000000..e3f7207
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/group-name-validarion.directive.ts
@@ -0,0 +1,37 @@
+/*
+ * 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 { Directive, forwardRef, Input } from '@angular/core';
+import { AbstractControl, NG_VALIDATORS, Validator } from '@angular/forms';
+
+@Directive({
+  selector: '[validator][ngModel],[group-dir][ngFormControl]',
+  providers: [{
+    multi: true,
+    provide: NG_VALIDATORS,
+    useExisting: forwardRef(() => GroupNameValidationDirective)
+  }]
+})
+export class GroupNameValidationDirective implements Validator {
+  @Input() validator: Function;
+
+  validate(control: AbstractControl): { [key: string]: any; } {
+    return this.validator(control);
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/manage-roles-groups.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/manage-roles-groups.component.html
new file mode 100644
index 0000000..85b751b
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/manage-roles-groups.component.html
@@ -0,0 +1,154 @@
+<!--
+  ~ 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.
+  -->
+
+<modal-dialog #bindDialog modalClass="manage-roles-dialog modal-xxl">
+  <modal-header>
+    <h4 class="modal-title">Manage roles</h4>
+  </modal-header>
+  <modal-content>
+    <div class="content-box">
+      <button mat-raised-button class="butt add-group" (click)="stepperView = !stepperView">
+        <i class="material-icons">people_outline</i>Add group
+      </button>
+      <mat-horizontal-stepper #stepper *ngIf="stepperView" class="stepper ani">
+        <mat-step>
+          <ng-template matStepLabel>Groups</ng-template>
+          <div class="inner-step mat-reset">
+            <input [validator]="groupValidarion()" type="text" placeholder="Enter group name" [(ngModel)]="setupGroup" #setupGroupName="ngModel">
+            <div class="danger_color" *ngIf="setupGroupName.errors?.patterns && setupGroupName.dirty">Group name can only contain letters, numbers, hyphens and '_'</div>
+            <div class="danger_color" *ngIf="setupGroupName.errors?.duplicate && setupGroupName.dirty">Group name already exists</div>
+          </div>
+          <div class="text-center m-bott-10">
+            <button mat-raised-button (click)="resetDialog()" class="butt">Cancel</button>
+            <button mat-raised-button matStepperNext class="butt">Next<i class="material-icons">keyboard_arrow_right</i></button>
+          </div>
+        </mat-step>
+        <mat-step>
+          <ng-template matStepLabel>Roles</ng-template>
+          <div class="inner-step mat-reset">
+            <div class="selector-wrapper">
+              <!-- <multi-select-dropdown (selectionChange)="onUpdate($event)" [type]="'role'" [items]="rolesList" [model]="setupRoles"></multi-select-dropdown> -->
+              <mat-form-field>
+                  <mat-select multiple [compareWith]="compareObjects" name="roles" [(value)]="setupRoles" placeholder="Select roles">
+                    <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>
+            </div>
+          </div>
+          <div class="text-center m-bott-10">
+            <button mat-raised-button matStepperPrevious class="butt"><i class="material-icons">keyboard_arrow_left</i>Back</button>
+            <button mat-raised-button (click)="resetDialog()" class="butt">Cancel</button>
+            <button mat-raised-button matStepperNext class="butt">Next<i class="material-icons">keyboard_arrow_right</i></button>
+          </div>
+        </mat-step>
+        <mat-step>
+          <ng-template matStepLabel>Users</ng-template>
+          <div class="inner-step mat-reset">
+            <input type="text" placeholder="Enter user login" [(ngModel)]="setupUser">
+          </div>
+          <div class="text-center m-bott-10">
+            <button mat-raised-button matStepperPrevious class="butt"><i 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?.pattern || !setupRoles.length > 0">Create</button>
+          </div>
+        </mat-step>
+      </mat-horizontal-stepper>
+      <mat-divider></mat-divider>
+      <div *ngIf="groupsData.length" class="ani">
+        <table class="dashboard_table">
+          <tr>
+            <th class="th_name groups">Name</th>
+            <th class="roles">Roles</th>
+            <th class="users">Users</th>
+            <th class="th_actions">Action</th>
+          </tr>
+          <tr *ngFor="let item of groupsData" class="dashboard_table_body filter-row">
+            <td>{{ item.group }}</td>
+            <td class="roles mat-reset">
+              <div class="selector-wrapper-edit">
+                <mat-form-field>
+                  <mat-select multiple [compareWith]="compareObjects" name="selected_roles" [(value)]="item.selected_roles" placeholder="Select roles">
+                    <mat-option class="multiple-select" disabled>
+                      <a class="select ani" (click)="selectAllOptions(item, rolesList, 'selected_roles')">
+                        <i class="material-icons">playlist_add_check</i>&nbsp;All
+                      </a>
+                      <a class="deselect ani" (click)="selectAllOptions(item, 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 ani">
+                    <i class="material-icons">keyboard_arrow_down</i>
+                  </button>
+                </mat-form-field>
+              </div>
+            </td>
+            <td class="users-list ani">
+              <mat-form-field class="chip-list">
+                <input #user matInput placeholder="Enter user login" pattern="[@.-_0-9a-zA-Z]" (keydown.enter)="addUser(user.value, item); user.value = ''">
+                <button mat-icon-button matSuffix (click)="addUser(user.value, item); user.value = ''">
+                  <mat-icon>person_add</mat-icon>
+                </button>
+              </mat-form-field>
+              <div class="list-selected list-container ani">
+                <mat-chip-list>
+                  <mat-chip *ngFor="let user of item.users">
+                    {{ user }}
+                    <a class="material-icons" (click)="removeUser(item.users, user)">clear</a>
+                  </mat-chip>
+                </mat-chip-list>
+              </div>
+            </td>
+            <td class="actions">
+              <button mat-icon-button class="reset ani" (click)="manageAction('delete', 'group', item)">
+                <i class="material-icons">close</i>
+              </button>
+
+              <button mat-icon-button class="apply ani" matTooltip="Group cannot be updated without any selected role"
+                      matTooltipPosition="above"
+                      [matTooltipDisabled]="item.selected_roles.length > 0"
+                      [ngClass]="{ 'not-allowed' : !item.selected_roles.length }"
+                      (click)="manageAction('update', 'group', item)">
+                <i class="material-icons">done</i>
+              </button>
+            </td>
+          </tr>
+        </table>
+      </div>
+    </div>
+  </modal-content>
+</modal-dialog>
+
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/manage-roles-groups.component.scss b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/manage-roles-groups.component.scss
new file mode 100644
index 0000000..b28c061
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/manage-roles-groups.component.scss
@@ -0,0 +1,292 @@
+/*!
+ * 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.
+ */
+
+::ng-deep .mat-option:first-child .mat-pseudo-checkbox { display: none; }
+
+.caret {
+  width: 40px;
+  color: #4ab8dc;
+  border: none;
+  border-left: 1px solid #ececec;
+  background-color: #fff;
+  position: absolute;
+  right: 0;
+  top: 0px;
+  height: 36px;
+  cursor: pointer;
+  &.not-allowed {
+    background-color: #dcdcdc;
+  }
+}
+
+
+.content-box {
+  padding: 20px 30px 35px;
+  height: 85vh;
+  overflow-y: auto;
+}
+.no-details {
+  color: #d8d8d8;
+}
+.mat-divider {
+  margin: 10px 0;
+}
+.stepper {
+  height: 190px;
+  margin-top: 10px;
+  .inner-step {
+    height: 70px;
+    padding: 5px;
+    display: flex;
+    justify-content: center;
+    flex-direction: column;
+    text-align: center;
+    input {
+      width: 490px;
+      align-self: center;
+    }
+    .caret {
+      i {
+        margin-top: 3px;
+      }
+    }
+  }
+  .text-center {
+    button {
+      &:not(:last-child) {
+        margin-right: 5px;
+      }
+    }
+  }
+}
+.selector-wrapper {
+  display: flex;
+  align-self: center;
+  width: 490px;
+  height: 36px;
+  padding-left: 10px;
+  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);
+  mat-form-field {
+    width: 100%;
+    .mat-form-field-wrapper {
+      padding-bottom: 0;
+    }
+    .mat-icon {
+      font-size: 20px;
+    }
+  }
+  .dropdown-multiselect {
+    width: 100% !important;
+    > button {
+      padding: 6px 22px;
+    }
+  }
+}
+.list-header {
+  padding: 0 15px;
+}
+.scrolling-content {
+  max-height: 450px;
+  overflow-x: hidden;
+  overflow-y: auto;
+  display: block;
+  padding: 15px 5px;
+  &.stepper-opened {
+    height: 250px;
+  }
+}
+.roles {
+  width: 30%;
+  padding: 0 10px;
+  .selector-wrapper-edit {
+    position: relative;
+    display: flex;
+    justify-content: space-between;
+    height: 36px;
+    padding-left: 10px;
+    background: #fff;
+    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-select-dropdown {
+      width: 100%;
+      .dropdown-multiselect {
+        > button {
+          padding: 8px 22px 5px;
+        }
+      }
+    }
+    .caret {
+      &:hover {
+        box-shadow: 0 3px 1px -10px rgba(0, 0, 0, 0.2), 0 2px 1px 0 rgba(0, 0, 0, 0.17), 0 1px 5px 0 rgba(0, 0, 0, 0.12)
+      }
+    }
+  }
+}
+.groups {
+  width: 20%;
+}
+.users {
+  width: 30%;
+  padding: 0 10px;
+}
+.users-list {
+  padding: 5px 10px;
+  font-family: 'Open Sans', sans-serif;
+  font-size: 15px;
+  font-weight: 300;
+  color: #577289;
+  position: relative;
+  i {
+    color: #FF5722;
+    font-size: 18px;
+    cursor: pointer;
+  }
+  .list-selected {
+    width: 100%;
+    margin-top: 50px;
+    height: inherit;
+    min-height: 0;
+    overflow-y: hidden;
+  }
+}
+.expanded-panel {
+  display: flex;
+  align-items: flex-end;
+  .add-input-block {
+    display: flex;
+    height: 36px;
+    padding-right: 0;
+    outline: none;
+    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);
+    input {
+      height: 36px;
+      padding: 0;
+      padding-left: 10px;
+      width: 170px;
+    }
+    .caret {
+      width: 50px;
+    }
+  }
+}
+
+
+mat-expansion-panel-header {
+  &.mat-expansion-panel-header {
+    font-family: 'Open Sans', sans-serif;
+    font-size: 15px;
+    font-weight: 300;
+    color: #577289;
+  }
+}
+
+.mat-step-header {
+  .mat-step-icon {
+    background-color: #36afd5 !important;
+  }
+  .mat-step-label {
+    font-family: 'Open Sans', sans-serif;
+    font-size: 16px;
+    font-weight: 300;
+  }
+}
+
+.dashboard_table {
+  .dashboard_table_body {
+    .actions {
+      padding: 10px 0;
+    }
+    &.filter-row {
+      .actions {
+        button {
+          background: none;
+          .mid {
+            vertical-align: sub;
+          }
+        }
+      }
+    }
+  }
+  .th_actions {
+    width: 10%;
+  }
+}
+
+.mat-chip:not(.mat-basic-chip) {
+  transition: box-shadow 280ms cubic-bezier(.4,0,.2,1);
+  padding: 7px 0 7px 10px;
+  border-radius: 24px;
+  cursor: default;
+  display: inline-block;
+  position: relative;
+  padding-right: 25px;
+  white-space: nowrap;
+  max-width: 100%;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  margin: 2px !important;
+}
+mat-chip.mat-chip a {
+  position: absolute;
+  right: 15px;
+}
+mat-form-field.chip-list {
+  &.mat-form-field {
+    position: absolute;
+    top: 2px;
+    width: 94%;
+    font-size: 16px;
+    font-weight: 300;
+  }
+}
+
+.mat-raised-button {
+  &.butt {
+    &.butt-success {
+      margin-left: 0;
+    }
+  }
+}
+.multiple-select {
+  border-bottom: 1px solid #dedbdb;
+  padding: 0;
+  a {
+    display: inline-block;
+    width: 50%;
+    padding: 0 15px;
+    vertical-align: middle;
+    color: #575757;
+    cursor: pointer;
+    i {
+      vertical-align: sub;
+      font-size: 20px;
+    }
+    &:hover {
+      color: #4eaf3e;
+      background: #f9fafb;
+    }
+    &.deselect {
+      &:hover {
+        color: #f1696e;
+      }
+    }
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/manage-roles-groups.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/manage-roles-groups.component.ts
new file mode 100644
index 0000000..bc91c94
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-roles-groups/manage-roles-groups.component.ts
@@ -0,0 +1,192 @@
+/*
+ * 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 { Component, OnInit, ViewChild, Output, EventEmitter, Inject } from '@angular/core';
+import { ValidatorFn, FormControl, NgModel } from '@angular/forms';
+import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
+import { DICTIONARY } from '../../../../dictionary/global.dictionary';
+
+@Component({
+  selector: 'dlab-manage-roles-groups',
+  templateUrl: './manage-roles-groups.component.html',
+  styleUrls: ['../../../resources/resources-grid/resources-grid.component.css', './manage-roles-groups.component.scss']
+})
+export class ManageRolesGroupsComponent implements OnInit {
+  readonly DICTIONARY = DICTIONARY;
+
+  public groupsData: Array<any> = [];
+  public roles: Array<any> = [];
+  public rolesList: Array<string> = [];
+  public setupGroup: string = '';
+  public setupUser: string = '';
+  public manageUser: string = '';
+  public setupRoles: Array<string> = [];
+  public updatedRoles: Array<string> = [];
+  public delimitersRegex = /[-_]?/g;
+  public groupnamePattern = new RegExp(/^[a-zA-Z0-9_\-]+$/);
+
+  @ViewChild('bindDialog') bindDialog;
+  @Output() manageRolesGroupAction: EventEmitter<{}> = new EventEmitter();
+  stepperView: boolean = false;
+
+  constructor(public dialog: MatDialog) { }
+
+  ngOnInit() {
+    this.bindDialog.onClosing = () => this.resetDialog();
+  }
+
+  public open(param, groups, roles): void {
+    this.roles = roles;
+    this.rolesList = roles.map(role => role.description);
+    this.updateGroupData(groups);
+
+    this.stepperView = false;
+    this.bindDialog.open(param);
+  }
+
+  public onUpdate($event) {
+    if ($event.type === 'role') {
+      this.setupRoles = $event.model;
+    } else {
+      this.updatedRoles = $event.model;
+    }
+    $event.$event.preventDefault();
+  }
+
+  public selectAllOptions(item, values, byKey?) {
+    byKey ? (item[byKey] = values ? values : []) : this.setupRoles = values ? values : [];
+  }
+
+  public manageAction(action: string, type: string, item?: any, value?) {
+    if (action === 'create') {
+      this.manageRolesGroupAction.emit(
+        { action, type, value: {
+          name: this.setupGroup,
+          users: this.setupUser ? this.setupUser.split(',').map(elem => elem.trim()) : [],
+          roleIds: this.extractIds(this.roles, this.setupRoles)
+        }
+      });
+      this.stepperView = false;
+    } else if (action === 'delete') {
+      const data = (type === 'users') ? {group: item.group, user: value} : {group: item.group, id: item};
+      const dialogRef: MatDialogRef<ConfirmDeleteUserAccountDialogComponent> = this.dialog.open(
+        ConfirmDeleteUserAccountDialogComponent,
+        { data: data, width: '550px', panelClass: 'error-modalbox' }
+      );
+
+      dialogRef.afterClosed().subscribe(result => {
+        if (result) {
+          const emitValue = (type === 'users')
+            ? {action, type, id: item.name, value: { user: value, group: item.group }}
+            : {action, type, id: item.name, value: item.group} ;
+          this.manageRolesGroupAction.emit(emitValue);
+        }
+      });
+    } else if (action === 'update') {
+      this.manageRolesGroupAction.emit({action, type, value: {
+        name: item.group,
+        roleIds: this.extractIds(this.roles, item.selected_roles),
+        users: item.users || [] }});
+    }
+    this.resetDialog();
+  }
+
+  public extractIds(sourceList, target) {
+    return sourceList.reduce((acc, item) => {
+      target.includes(item.description) && acc.push(item._id);
+      return acc;
+    }, []);
+  }
+
+  public updateGroupData(groups) {
+    this.groupsData = groups;
+
+    this.groupsData.forEach(item => {
+      item.selected_roles = item.roles.map(role => role.description);
+    });
+  }
+
+  public groupValidarion(): ValidatorFn {
+
+    const duplicateList: any = this.groupsData.map(item => item.group);
+    return <ValidatorFn>((control: FormControl) => {
+      if (control.value && duplicateList.includes(this.delimitersFiltering(control.value)))
+        return { duplicate: true };
+
+      if (control.value && !this.groupnamePattern.test(control.value))
+        return { patterns: true };
+
+      return null;
+    });
+  }
+
+  public compareObjects(o1: any, o2: any): boolean {
+    return o1.toLowerCase() === o2.toLowerCase();
+  }
+
+  public delimitersFiltering(resource): string {
+    return resource.replace(this.delimitersRegex, '').toString().toLowerCase();
+  }
+
+  public resetDialog() {
+    this.stepperView = false;
+    this.setupGroup = '';
+    this.setupUser = '';
+    this.manageUser = '';
+    this.setupRoles = [];
+    this.updatedRoles = [];
+  }
+
+  public removeUser(list, item): void {
+    list.splice(list.indexOf(item), 1);
+  }
+
+  public addUser(value: string, item): void {
+    if (value && value.trim()) {
+      item.users instanceof Array ? item.users.push(value.trim()) : item.users = [value.trim()];
+    }
+  }
+}
+
+
+@Component({
+  selector: 'dialog-result-example-dialog',
+  template: `
+  <div class="dialog-header">
+    <h4 class="modal-title"><i class="material-icons">priority_high</i>Warning</h4>
+    <button type="button" class="close" (click)="dialogRef.close()">&times;</button>
+  </div>
+  <div mat-dialog-content class="content">
+    <p *ngIf="data.user">User <strong>{{ data.user }}</strong> will be deleted from <strong>{{ data.group }}</strong> group.</p>
+    <p *ngIf="data.id">Group <strong>{{ data.group }}</strong> will be decommissioned.</p>
+    <p class="m-top-20"><strong>Do you want to proceed?</strong></p>
+  </div>
+  <div class="text-center">
+    <button type="button" class="butt" mat-raised-button (click)="dialogRef.close()">No</button>
+    <button type="button" class="butt butt-success" mat-raised-button (click)="dialogRef.close(true)">Yes</button>
+  </div>
+  `,
+  styles: []
+})
+export class ConfirmDeleteUserAccountDialogComponent {
+  constructor(
+    public dialogRef: MatDialogRef<ConfirmDeleteUserAccountDialogComponent>,
+    @Inject(MAT_DIALOG_DATA) public data: any
+  ) { }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.html
new file mode 100644
index 0000000..6b8bb95
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.html
@@ -0,0 +1,161 @@
+<!--
+  ~ 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.
+  -->
+
+<table class="dashboard_table" *ngIf="isAdmin; else users">
+  <tr>
+    <th class="th_user">User</th>
+    <th class="th_name">Name</th>
+    <th class="th_status">Status</th>
+    <th class="th_shape">Shape / Resource id</th>
+    <th class="th_resources">Computational resources</th>
+    <th class="th_actions">Action</th>
+  </tr>
+
+  <tr *ngFor="let env of allEnvironmentData" class="dashboard_table_body">
+    <td>{{ env.user }}</td>
+    <td [ngClass]="{'capitalize': env.name === 'edge node'}">{{ env.name }}</td>
+    <td class="status" ngClass="{{env.status || ''}}">{{env.status}}</td>
+    <td>{{ env.shape || env.ip }}</td>
+    <td>
+      <div class="source" *ngIf="env.resources">
+        <div *ngIf="!env.resources?.length">
+          <span *ngIf="!env.resources.length" class="no-details">no details</span>
+        </div>
+        <div *ngIf="env.resources?.length">
+          <div *ngFor="let resource of env.resources" class="resource-wrap">
+            <div class="resource-name">
+              <a class="detailed-link">
+                {{ resource.computational_name }}
+              </a>
+            </div>
+            <span ngClass="{{resource.status || ''}}" class="resource-status">{{ resource.status }}</span>
+            <div class="resource-actions">
+              <a class="start-stop-action" *ngIf="resource.image === 'docker.dlab-dataengine'">
+                <i class="material-icons" (click)="toggleResourceAction(env, 'stop', resource)"
+                  [ngClass]="{'not-allowed' : resource.status !== 'running' || env.user.toLowerCase() === currentUser && healthStatus !== 'ok' }">pause_circle_outline</i>
+              </a>
+
+              <a class="remove_butt" (click)="toggleResourceAction(env, 'terminate', resource)"
+                  [ngClass]="{ 'disabled' : env.status !== 'running' || resource.status !== 'running'
+                              && resource.status !== 'stopped' || env.user.toLowerCase() === currentUser && healthStatus !== 'ok' }">
+                <i class="material-icons">highlight_off</i>
+              </a>
+            </div>
+          </div>
+        </div>
+      </div>
+    </td>
+    <td class="settings">
+      <span #settings class="actions" (click)="actions.toggle($event, settings)" [ngClass]="{ 'disabled'
+            : (!uploadKey && env.type === 'notebook' && env.user.toLowerCase() === currentUser)
+            || (env.status !== 'running' && env.status !== 'stopped' && env.status !== 'stopping' && env.status !== 'failed' )
+            || (env.type === 'edge node' && env.user.toLowerCase() === currentUser && env.status === 'stopping')
+            || env.type === 'edge node' && env.user.toLowerCase() !== currentUser && env.status !== 'running'
+            || env.user.toLowerCase() === currentUser && healthStatus !== 'ok' && env.type !== 'edge node' }"></span>
+      <bubble-up #actions class="list-menu" position="bottom-left" alternative="top-left">
+        <ul class="list-unstyled">
+          <li
+            matTooltip="{{ env.type !== 'edge node' ? 'Unable to stop notebook because at least one computational resource is in progress' : 'Unable to stop edge node because at least one resource of this user is in progress' }}"
+            matTooltipPosition="above" [matTooltipDisabled]="!isResourcesInProgress(env)"
+            [hidden]="env.name === 'edge node' && env.status === 'stopped'">
+            <div (click)="toggleResourceAction(env, 'stop')"
+              [ngClass]="{'not-allowed' : env.status === 'stopped' || env.status === 'stopping' || env.status === 'starting' || env.status === 'creating image' || env.status === 'failed' || isResourcesInProgress(env)}">
+              <i class="material-icons">pause_circle_outline</i>
+              <span>Stop</span>
+            </div>
+          </li>
+          <li *ngIf="env.name !== 'edge node'"
+            matTooltip="Unable to terminate notebook because at least one computational resource is in progress"
+            matTooltipPosition="above" [matTooltipDisabled]="!isResourcesInProgress(env)">
+            <div (click)="toggleResourceAction(env, 'terminate')"
+              [ngClass]="{'not-allowed' : env.status !== 'running' && env.status !== 'stopped' || isResourcesInProgress(env)}">
+              <i class="material-icons">phonelink_off</i>
+              <span>Terminate</span>
+            </div>
+          </li>
+
+          <div *ngIf="env.name === 'edge node' && env.user.toLowerCase() === currentUser">
+            <li (click)="toggleResourceAction(env, 'run')" *ngIf="env.status === 'stopped'">
+              <i class="material-icons">play_circle_outline</i>
+              <span>Start</span>
+            </li>
+            <li (click)="toggleResourceAction(env, 'recreate')" *ngIf="env.status === 'terminated'">
+              <i class="material-icons">refresh</i>
+              <span>Recreate</span>
+            </li>
+            <li *ngIf="!isResourcesInProgress(env)" (click)="showReuploaKeydDialog()">
+              <i class="material-icons">sync_problem</i>
+              <span>Reupload key</span>
+            </li>
+          </div>
+        </ul>
+      </bubble-up>
+    </td>
+  </tr>
+</table>
+
+<ng-template #users>
+  <table class="dashboard_table">
+    <tr>
+      <th class="th_name">Type</th>
+      <th>Resource id</th>
+      <th class="th_status">Status</th>
+      <th class="th_actions">Action</th>
+    </tr>
+    <tr *ngFor="let env of environmentsHealthStatuses" class="dashboard_table_body">
+      <td>{{env.type}}</td>
+      <td>{{env.resource_id}}</td>
+      <td class="status" ngClass="{{env.status || ''}}">{{env.status}}</td>
+      <td class="settings">
+        <span #settings (click)="actions.toggle($event, settings)" class="actions" [ngClass]="{'disabled': env.status !== 'running'
+              && env.status !== 'stopped'
+              && env.status !== 'terminated'
+              || notebookInProgress
+              || !uploadKey && env.status !== 'failed' }">
+        </span>
+
+        <bubble-up #actions class="list-menu" position="bottom-left" alternative="top-left">
+          <ul class="list-unstyled">
+            <li (click)="toggleResourceAction(env, 'stop')" *ngIf="env.status === 'running'">
+              <i class="material-icons">pause_circle_outline</i>
+              <span>Stop</span>
+            </li>
+            <li (click)="toggleResourceAction(env, 'run')" *ngIf="env.status === 'stopped'">
+              <i class="material-icons">play_circle_outline</i>
+              <span>Start</span>
+            </li>
+            <li (click)="toggleResourceAction(env, 'recreate')" *ngIf="env.status === 'terminated'">
+              <i class="material-icons">refresh</i>
+              <span>Recreate</span>
+            </li>
+            <li *ngIf="!anyEnvInProgress" (click)="showReuploaKeydDialog()">
+              <i class="material-icons">sync_problem</i>
+              <span>Reupload key</span>
+            </li>
+          </ul>
+        </bubble-up>
+      </td>
+    </tr>
+  </table>
+</ng-template>
+
+<confirmation-dialog #confirmationDialog [manageAction]="isAdmin" (buildGrid)="buildGrid()"></confirmation-dialog>
+<key-upload-dialog #keyReuploadDialog [primaryUploading]="false" (checkInfrastructureCreationProgress)="buildGrid()"
+  (generateUserKey)="generateUserKey()">
+</key-upload-dialog>
\ No newline at end of file
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.scss b/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.scss
new file mode 100644
index 0000000..6198578
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.scss
@@ -0,0 +1,40 @@
+/*!
+ * 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.
+ */
+
+.dashboard_table {
+  .th_user,
+  .th_name {
+    width: 20%;
+  }
+  .th_shape,
+  .th_status {
+    width: 12% !important;
+  }
+  .th_resources {
+    width: 33%;
+  }
+  .dashboard_table_body {
+    td:first-child {
+      cursor: default;
+    }
+  }
+}
+.source .resource-wrap .resource-name .detailed-link {
+  cursor: default !important;
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.ts
new file mode 100644
index 0000000..7cc28e8
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.ts
@@ -0,0 +1,172 @@
+/*
+ * 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 { Component, OnInit, ViewChild, Input, Output, EventEmitter, Inject } from '@angular/core';
+import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
+import { ToastrService } from 'ngx-toastr';
+
+import { HealthStatusService, UserAccessKeyService } from '../../../core/services';
+import { ConfirmationDialogType } from '../../../shared';
+import { FileUtils } from '../../../core/util';
+
+export interface ManageAction {
+  action: string;
+  environment: string;
+  resource: string;
+}
+
+@Component({
+  selector: 'management-grid',
+  templateUrl: 'management-grid.component.html',
+  styleUrls: [
+    './management-grid.component.scss',
+    '../../../resources/resources-grid/resources-grid.component.css',
+    '../../../resources/computational/computational-resources-list/computational-resources-list.component.scss'
+  ]
+})
+export class ManagementGridComponent implements OnInit {
+  @Input() allEnvironmentData: Array<any>;
+  @Input() environmentsHealthStatuses: Array<any>;
+  @Input() healthStatus: string;
+  @Input() resources: Array<any>;
+  @Input() uploadKey: boolean;
+  @Input() isAdmin: boolean;
+  @Input() currentUser: string = '';
+  @Output() refreshGrid: EventEmitter<{}> = new EventEmitter();
+  @Output() actionToggle: EventEmitter<ManageAction> = new EventEmitter();
+
+  @ViewChild('confirmationDialog') confirmationDialog;
+  @ViewChild('keyReuploadDialog') keyReuploadDialog;
+
+  constructor(
+    private healthStatusService: HealthStatusService,
+    private userAccessKeyService: UserAccessKeyService,
+    public toastr: ToastrService,
+    public dialog: MatDialog
+  ) {}
+
+  ngOnInit() {
+  }
+
+  buildGrid(): void {
+    this.refreshGrid.emit();
+  }
+
+  toggleResourceAction(environment, action: string, resource?) {
+    if (resource) {
+      const resource_name = resource ? resource.computational_name : environment.name;
+      const dialogRef: MatDialogRef<ConfirmationDialogComponent> = this.dialog.open(ConfirmationDialogComponent, {
+        data: { action, resource_name, user: environment.user },
+        width: '550px',
+        panelClass: 'error-modalbox'
+      });
+      dialogRef.afterClosed().subscribe(result => {
+        result && this.actionToggle.emit({ action, environment, resource });
+      });
+    } else {
+      if (action === 'stop') {
+        this.confirmationDialog.open(
+          { isFooter: false },
+          environment,
+          (environment.name === 'edge node' || environment.type.toLowerCase() === 'edge node')
+            ? ConfirmationDialogType.StopEdgeNode
+            : ConfirmationDialogType.StopExploratory,
+          );
+      } else if (action === 'terminate') {
+        this.confirmationDialog.open({ isFooter: false }, environment, ConfirmationDialogType.TerminateExploratory);
+      } else if (action === 'run') {
+        this.healthStatusService
+          .runEdgeNode()
+          .subscribe(() => {
+            this.buildGrid();
+            this.toastr.success('Edge node is starting!', 'Processing!');
+          }, error => this.toastr.error('Edge Node running failed!', 'Oops!'));
+        } else if (action === 'recreate') {
+          this.healthStatusService
+            .recreateEdgeNode()
+            .subscribe(() => {
+              this.buildGrid();
+              this.toastr.success('Edge Node recreation is processing!', 'Processing!');
+            }, error => this.toastr.error('Edge Node recreation failed!', 'Oops!'));
+        }
+    }
+  }
+
+  isResourcesInProgress(notebook) {
+    if (notebook) {
+      if (notebook.name === 'edge node') {
+        return this.allEnvironmentData
+          .filter(env => env.user === notebook.user)
+          .some(el => this.inProgress([el]) || this.inProgress(el.resources));
+      } else if (notebook.resources.length) {
+        return this.inProgress(notebook.resources);
+      }
+    }
+    return false;
+  }
+
+  inProgress(resources) {
+    return resources.filter(resource => (
+      resource.status !== 'failed'
+      && resource.status !== 'terminated'
+      && resource.status !== 'running'
+      && resource.status !== 'stopped')).length > 0;
+  }
+
+  showReuploaKeydDialog() {
+    this.keyReuploadDialog.open({ isFooter: false });
+  }
+
+  public generateUserKey() {
+    this.userAccessKeyService.regenerateAccessKey().subscribe(data => {
+      FileUtils.downloadFile(data);
+      this.buildGrid();
+    });
+  }
+}
+
+
+@Component({
+  selector: 'confirm-dialog',
+  template: `
+  <div class="dialog-header">
+    <h4 class="modal-title"><span class="capitalize">{{ data.action }}</span> resource</h4>
+    <button type="button" class="close" (click)="dialogRef.close()">&times;</button>
+  </div>
+  <div mat-dialog-content class="content">
+    <p>Resource <strong> {{ data.resource_name }}</strong> of user <strong> {{ data.user }} </strong> will be
+      <span *ngIf="data.action === 'terminate'"> decommissioned.</span>
+      <span *ngIf="data.action === 'stop'">stopped.</span>
+    </p>
+    <p class="m-top-20"><strong>Do you want to proceed?</strong></p>
+  </div>
+  <div class="text-center">
+    <button type="button" class="butt" mat-raised-button (click)="dialogRef.close()">No</button>
+    <button type="button" class="butt butt-success" mat-raised-button (click)="dialogRef.close(true)">Yes</button>
+  </div>
+  `,
+  styles: [
+  ]
+})
+export class ConfirmationDialogComponent {
+  constructor(
+    public dialogRef: MatDialogRef<ConfirmationDialogComponent>,
+    @Inject(MAT_DIALOG_DATA) public data: any
+  ) {}
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/management.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/management/management.component.html
new file mode 100644
index 0000000..b0b2788
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/management.component.html
@@ -0,0 +1,55 @@
+<!--
+  ~ 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="sub-nav base-retreat">
+    <div *ngIf="healthStatus?.admin" class="admin-group">
+        <button mat-raised-button class="butt ssn" (click)="openManageRolesDialog()">
+        <i class="material-icons"></i>Manage roles
+      </button>
+        <button mat-raised-button class="butt ssn" (click)="openSsnMonitorDialog()">
+        <i class="material-icons"></i>SSN Monitor
+      </button>
+        <button mat-raised-button class="butt env" (click)="openManageEnvironmentDialog()">
+        <i class="material-icons"></i>Manage environment
+      </button>
+        <button mat-raised-button class="butt" (click)="showBackupDialog()" [disabled]="creatingBackup">
+        <i class="material-icons">backup</i>Backup
+      </button>
+    </div>
+    <button mat-raised-button class="butt" (click)="buildGrid()">
+    <i class="material-icons">autorenew</i>Refresh
+  </button>
+</div>
+
+<management-grid [currentUser]="user.toLowerCase()" [uploadKey]="uploadKey" [healthStatus]="healthStatus?.status" [isAdmin]="healthStatus?.admin" [allEnvironmentData]="allEnvironmentData" [environmentsHealthStatuses]="healthStatus?.list_resources" (refreshGrid)="buildGrid()"
+    (actionToggle)="manageEnvironmentAction($event)">
+</management-grid>
+
+
+<dlab-backup-dilog #backupDialog (backupOpts)="createBackup($event)">
+</dlab-backup-dilog>
+
+<dlab-manage-env-dilog #manageEnvDialog (manageEnv)="manageEnvironment($event)" (setBudget)="setBudgetLimits($event)">
+</dlab-manage-env-dilog>
+
+<progress-dialog #preloaderModal></progress-dialog>
+<dlab-ssn-monitor #ssnMonitor></dlab-ssn-monitor>
+
+<dlab-manage-roles-groups #rolesGroupsModal (manageRolesGroupAction)="manageRolesGroups($event)">
+</dlab-manage-roles-groups>
\ No newline at end of file
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/management.component.scss b/services/self-service/src/main/resources/webapp/src/app/administration/management/management.component.scss
new file mode 100644
index 0000000..ee18db9
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/management.component.scss
@@ -0,0 +1,37 @@
+/*!
+ * 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.
+ */
+
+.sub-nav {
+  justify-content: flex-end;
+  .admin-group {
+    margin-right: 10px;
+  }
+  button {
+    margin-right: 10px;
+    &:last-child {
+      margin-right: 0;
+    }
+    &.env {
+      width: 210px;
+    }
+    &.ssn {
+      width: inherit;
+    }
+  }
+}
\ No newline at end of file
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/management.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/management/management.component.ts
new file mode 100644
index 0000000..d898b84
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/management.component.ts
@@ -0,0 +1,275 @@
+/*
+ * 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 { Component, OnInit, ViewChild, OnDestroy } from '@angular/core';
+import { ToastrService } from 'ngx-toastr';
+import { Subscription } from 'rxjs';
+
+import {
+  HealthStatusService,
+  ManageEnvironmentsService,
+  UserAccessKeyService,
+  BackupService,
+  UserResourceService,
+  RolesGroupsService,
+  StorageService
+} from '../../core/services';
+
+import { EnvironmentModel, GeneralEnvironmentStatus } from './management.model';
+import { HTTP_STATUS_CODES } from '../../core/util';
+
+@Component({
+  selector: 'environments-management',
+  templateUrl: './management.component.html',
+  styleUrls: ['./management.component.scss']
+})
+export class ManagementComponent implements OnInit, OnDestroy {
+  public user: string = '';
+  public healthStatus: GeneralEnvironmentStatus;
+  public allEnvironmentData: Array<EnvironmentModel>;
+  public uploadKey: boolean = true;
+  public anyEnvInProgress: boolean = false;
+  public notebookInProgress: boolean = false;
+
+  private subscriptions: Subscription = new Subscription();
+  private clear = undefined;
+
+  @ViewChild('backupDialog') backupDialog;
+  @ViewChild('manageEnvDialog') manageEnvironmentDialog;
+  @ViewChild('keyUploadModal') keyUploadDialog;
+  @ViewChild('preloaderModal') preloaderDialog;
+  @ViewChild('ssnMonitor') ssnMonitorDialog;
+  @ViewChild('rolesGroupsModal') rolesGroupsDialog;
+
+  constructor(
+    private healthStatusService: HealthStatusService,
+    private backupService: BackupService,
+    private manageEnvironmentsService: ManageEnvironmentsService,
+    private userAccessKeyService: UserAccessKeyService,
+    private userResourceService: UserResourceService,
+    private rolesService: RolesGroupsService,
+    private storageService: StorageService,
+    public toastr: ToastrService
+  ) {}
+
+  ngOnInit() {
+    this.buildGrid();
+    this.user = this.storageService.getUserName();
+    this.subscriptions.add(this.userAccessKeyService.accessKeyEmitter
+      .subscribe(result => this.uploadKey = (result && result.status === 200)));
+
+    this.subscriptions.add(this.userAccessKeyService.keyUploadProccessEmitter.subscribe(response => {
+      if (response) this.buildGrid();
+    }));
+  }
+
+  ngOnDestroy(): void {
+    this.subscriptions.unsubscribe();
+  }
+
+  public buildGrid() {
+    this.getEnvironmentHealthStatus();
+  }
+
+  public manageEnvironmentAction($event) {
+    this.manageEnvironmentsService
+      .environmentManagement(
+        $event.environment.user,
+        $event.action,
+        $event.environment.name === 'edge node' ? 'edge' : $event.environment.name,
+        $event.resource ? $event.resource.computational_name : null
+      ).subscribe(
+        () => this.buildGrid(),
+        error => this.toastr.error('Environment management failed!', 'Oops!'));
+  }
+
+  showBackupDialog() {
+    if (!this.backupDialog.isOpened) this.backupDialog.open({ isFooter: false });
+  }
+
+  getActiveUsersList() {
+    return this.healthStatusService.getActiveUsers();
+  }
+
+  getTotalBudgetData() {
+    return this.healthStatusService.getTotalBudgetData();
+  }
+
+  openManageEnvironmentDialog() {
+    this.getActiveUsersList().subscribe(usersList => {
+      this.getTotalBudgetData().subscribe(total => this.manageEnvironmentDialog.open({ isFooter: false }, usersList, total));
+    },
+    () => this.toastr.error('Failed users list loading!', 'Oops!'));
+  }
+
+  openSsnMonitorDialog() {
+    this.healthStatusService.getSsnMonitorData()
+      .subscribe(data => this.ssnMonitorDialog.open({ isFooter: false }, data),
+      () => this.toastr.error('Failed ssn data loading!', 'Oops!'));
+  }
+
+  openManageRolesDialog() {
+    this.rolesService.getGroupsData().subscribe(group => {
+        this.rolesService.getRolesData().subscribe(
+          roles => this.rolesGroupsDialog.open({ isFooter: false }, group, roles),
+          error => this.toastr.error(error.message, 'Oops!'));
+      },
+      error => this.toastr.error(error.message, 'Oops!'));
+  }
+
+  isEnvironmentsInProgress(data): boolean {
+    return data.exploratory.some(el => {
+      return el.status === 'creating' || el.status === 'starting' ||
+        el.computational_resources.some(elem => elem.status === 'creating' || elem.status === 'starting' || elem.status === 'configuring');
+    });
+  }
+
+  isNotebookInProgress(data): boolean {
+    return data.exploratory.some(el => el.status === 'creating');
+  }
+
+  createBackup($event) {
+    this.backupService.createBackup($event).subscribe(result => {
+      this.getBackupStatus(result);
+      this.toastr.success('Backup configuration is processing!', 'Processing!');
+      this.clear = window.setInterval(() => this.getBackupStatus(result), 3000);
+    },
+    error => this.toastr.error(error.message, 'Oops!'));
+  }
+
+  manageRolesGroups($event) {
+    switch ($event.action) {
+      case 'create':
+        this.rolesService.setupNewGroup($event.value).subscribe(res => {
+          this.toastr.success('Group creation success!', 'Created!');
+          this.getGroupsData();
+        }, () => this.toastr.error('Group creation failed!', 'Oops!'));
+        break;
+      case 'update':
+        this.rolesService.updateGroup($event.value).subscribe(res => {
+          this.toastr.success('Group data successfully updated!', 'Success!');
+          this.getGroupsData();
+        }, () => this.toastr.error('Failed group data updating!', 'Oops!'));
+        break;
+      case 'delete':
+        if ($event.type === 'users') {
+          this.rolesService.removeUsersForGroup($event.value).subscribe(res => {
+            this.toastr.success('Users was successfully deleted!', 'Success!');
+            this.getGroupsData();
+          }, () => this.toastr.error('Failed users deleting!', 'Oops!'));
+        } else if ($event.type === 'group') {
+          console.log('delete group');
+          this.rolesService.removeGroupById($event.value).subscribe(res => {
+            this.toastr.success('Group was successfully deleted!', 'Success!');
+            this.getGroupsData();
+          }, () => this.toastr.error('Failed group deleting!', 'Oops!'));
+        }
+        break;
+      default:
+    }
+  }
+
+  setBudgetLimits($event) {
+    this.healthStatusService.updateUsersBudget($event.users).subscribe((result: any) => {
+      this.healthStatusService.updateTotalBudgetData($event.total).subscribe((res: any) => {
+        result.status === HTTP_STATUS_CODES.OK
+        && res.status === HTTP_STATUS_CODES.NO_CONTENT
+        && this.toastr.success('Budget limits updated!', 'Success!');
+        this.buildGrid();
+      });
+    }, error => this.toastr.error(error.message, 'Oops!'));
+  }
+
+  getGroupsData() {
+    this.rolesService.getGroupsData().subscribe(
+      list => this.rolesGroupsDialog.updateGroupData(list),
+      error => this.toastr.error(error.message, 'Oops!'));
+  }
+
+  manageEnvironment(event: {action: string, user: string}) {
+    this.healthStatusService
+      .manageEnvironment(event.action, event.user)
+      .subscribe(res => {
+          this.getActiveUsersList().subscribe(usersList => {
+            this.manageEnvironmentDialog.usersList = usersList;
+            this.toastr.success(`Action ${ event.action } is processing!`, 'Processing!');
+            this.buildGrid();
+          });
+        },
+      error => this.toastr.error(error.message, 'Oops!'));
+  }
+
+  private getExploratoryList() {
+    this.userResourceService.getUserProvisionedResources()
+      .subscribe((result) => {
+        this.anyEnvInProgress = this.isEnvironmentsInProgress(result);
+        this.notebookInProgress = this.isNotebookInProgress(result);
+      });
+  }
+
+  private getBackupStatus(result) {
+    const uuid = result.body;
+    this.backupService.getBackupStatus(uuid)
+      .subscribe((backupStatus: any) => {
+        if (!this.creatingBackup) {
+          backupStatus.status === 'FAILED'
+          ? this.toastr.error('Backup configuration failed!', 'Oops!')
+          : this.toastr.success('Backup configuration completed!', 'Success!');
+          clearInterval(this.clear);
+        }
+      }, () => {
+        clearInterval(this.clear);
+        this.toastr.error('Backup configuration failed!', 'Oops!');
+      });
+  }
+
+  get creatingBackup(): boolean {
+    return this.backupService.inProgress;
+  }
+
+  private getAllEnvironmentData() {
+    this.manageEnvironmentsService
+      .getAllEnvironmentData()
+      .subscribe((result: Array<EnvironmentModel>) => this.allEnvironmentData = this.loadEnvironmentList(result));
+  }
+
+  private loadEnvironmentList(data): Array<EnvironmentModel> {
+    if (data)
+      return data.map(value => new EnvironmentModel(
+          value.resource_name || value.resource_type,
+          value.status,
+          value.shape,
+          value.computational_resources,
+          value.user,
+          value.public_ip,
+          value.resource_type
+        ));
+  }
+
+  private getEnvironmentHealthStatus() {
+    this.healthStatusService
+      .getEnvironmentStatuses()
+      .subscribe((status: GeneralEnvironmentStatus) => {
+        this.healthStatus = status;
+        this.healthStatus.admin && this.getAllEnvironmentData();
+        this.userAccessKeyService.initialUserAccessKeyCheck();
+        this.getExploratoryList();
+      });
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/management.model.ts b/services/self-service/src/main/resources/webapp/src/app/administration/management/management.model.ts
new file mode 100644
index 0000000..51ab721
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/management.model.ts
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+
+export class EnvironmentModel {
+  constructor(
+    public name: string,
+    public status: string,
+    public shape: string,
+    public resources: Array<any>,
+    public user: string,
+    public ip: string,
+    public type?: string
+  ) { }
+}
+
+export class BackupOptionsModel {
+  constructor(
+    public configFiles: Array<string>,
+    public keys:  Array<string>,
+    public certificates:  Array<string>,
+    public jars:  Array<string>,
+    public databaseBackup: boolean,
+    public logsBackup: boolean
+  ) { }
+
+  setDegault(): void {
+    this.configFiles = ['all'];
+    this.keys = ['all'];
+    this.certificates = ['skip'];
+    this.jars = ['skip'];
+    this.databaseBackup = true;
+    this.logsBackup = false;
+  }
+}
+
+export interface GeneralEnvironmentStatus {
+  admin: boolean;
+  billingEnabled: boolean;
+  billingQuoteUsed: number;
+  list_resources: any;
+  status: string;
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/ssn-monitor/ssn-monitor.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/management/ssn-monitor/ssn-monitor.component.html
new file mode 100644
index 0000000..3d13266
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/ssn-monitor/ssn-monitor.component.html
@@ -0,0 +1,117 @@
+<!--
+  ~ 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.
+  -->
+
+<modal-dialog #bindDialog modalClass="ssn-monitor-dialog modal-lg">
+  <modal-header>
+    <h4 class="modal-title">SSN Monitor</h4>
+  </modal-header>
+  <modal-content>
+    <div class="content-box" *ngIf="monitorData">
+      <div class="ssn-info">
+        <mat-tab-group *ngIf="monitorData?.processorInfo" [dynamicHeight]="true">
+          <mat-tab label="CPU">
+            <div class="scrolling-content" id="scrolling">
+              <mat-list-item class="list-header">
+                <div class="col">Name</div>
+                <div class="col">{{ monitorData.processorInfo.name }}</div>
+              </mat-list-item>
+              <mat-list-item class="list-header">
+                <div class="col">Vendor</div>
+                <div class="col">{{ monitorData.processorInfo.vendor }}</div>
+              </mat-list-item>
+              <mat-list-item class="list-header">
+                <div class="col">Logical Core Count</div>
+                <div class="col">{{ monitorData.processorInfo.logicalCoreCount }}</div>
+              </mat-list-item>
+              <mat-list-item class="list-header">
+                <div class="col">Physical Core Count</div>
+                <div class="col">{{ monitorData.processorInfo.physicalCoreCount }}</div>
+              </mat-list-item>
+              <mat-list-item class="list-header">
+                <div class="col">Current System Load</div>
+                <div class="col">{{ monitorData.processorInfo.currentSystemLoad /100 | percent:'1.0-2' }}</div>
+              </mat-list-item>
+              <mat-list-item class="list-header">
+                <div class="col">System Load Average</div>
+                <div class="col">{{ monitorData.processorInfo.systemLoadAverage /100 | percent:'1.0-2' }}</div>
+              </mat-list-item>
+              <mat-list-item class="list-header">
+                <div class="col">CPU 64 Bit</div>
+                <div class="col">{{ monitorData.processorInfo.cpu64Bit }}</div>
+              </mat-list-item>
+            </div>
+          </mat-tab>
+
+          <mat-tab label="Memory">
+            <div class="scrolling-content" id="scrolling">
+              <mat-list-item class="list-header">
+                <div class="col">Available Memory</div>
+                <div class="col">{{ convertSize(monitorData.memoryInfo.availableMemory) }}</div>
+              </mat-list-item>
+              <mat-list-item class="list-header">
+                <div class="col">Total Memory</div>
+                <div class="col">{{ convertSize(monitorData.memoryInfo.totalMemory) }}</div>
+              </mat-list-item>
+              <mat-list-item class="list-header">
+                <div class="col">Swap Total</div>
+                <div class="col">{{ convertSize(monitorData.memoryInfo.swapTotal) }}</div>
+              </mat-list-item>
+              <mat-list-item class="list-header">
+                <div class="col">Swap Used</div>
+                <div class="col">{{ convertSize(monitorData.memoryInfo.swapUsed) }}</div>
+              </mat-list-item>
+              <mat-list-item class="list-header">
+                <div class="col">Pages Page In</div>
+                <div class="col">{{ convertSize(monitorData.memoryInfo.pagesPageIn) }}</div>
+              </mat-list-item>
+              <mat-list-item class="list-header">
+                <div class="col">Pages Page Out</div>
+                <div class="col">{{ convertSize(monitorData.memoryInfo.pagesPageOut) }}</div>
+              </mat-list-item>
+            </div>
+
+          </mat-tab>
+          <mat-tab label="HDD">
+            <div class="scrolling-content" id="scrolling">
+              <div *ngFor="let disk of monitorData.disksInfo; let i = index">
+                <mat-list-item>
+                  <div class="col"><strong>Disk {{i +1}}</strong></div>
+                </mat-list-item>
+                <mat-list-item>
+                  <div class="col">Used Space</div>
+                  <div class="col">{{ convertSize(disk.usedByteSpace) }}</div>
+                </mat-list-item>
+                <mat-list-item>
+                  <div class="col">Total Space</div>
+                  <div class="col">{{ convertSize(disk.totalByteSpace) }}</div>
+                </mat-list-item>
+              </div>
+            </div>
+          </mat-tab>
+        </mat-tab-group>
+        <div class="text-center">
+          <button type="button" class="butt" mat-raised-button (click)="close()">Close</button>
+        </div>
+      </div>
+      <div class="info message" *ngIf="isEmpty(monitorData)">
+        No ssn monitor data available
+      </div>
+    </div>
+  </modal-content>
+</modal-dialog>
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/ssn-monitor/ssn-monitor.component.scss b/services/self-service/src/main/resources/webapp/src/app/administration/management/ssn-monitor/ssn-monitor.component.scss
new file mode 100644
index 0000000..769fefe
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/ssn-monitor/ssn-monitor.component.scss
@@ -0,0 +1,50 @@
+/*!
+ * 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.
+ */
+
+.ssn-monitor-dialog {
+  .content-box {
+    padding-top: 10px !important;
+  }
+  .ssn-info {
+    min-height: 400px;
+    max-height: 500px;
+    .scrolling-content {
+      max-height: 310px;
+      overflow-y: auto;
+    }
+    .text-center {
+      position: absolute;
+      bottom: 20px;
+      left: 0;
+      right: 0;
+    }
+  }
+  .mat-list-item-content {
+    display: flex;
+    justify-content: initial;
+    color: #577289;
+    padding: 15px 5px;
+    border-bottom: 1px solid #f3f2f2;
+    font-size: 15px;
+    .col {
+      width: 50%;
+      font-weight: 300;
+     }
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/ssn-monitor/ssn-monitor.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/management/ssn-monitor/ssn-monitor.component.ts
new file mode 100644
index 0000000..3d95499
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/ssn-monitor/ssn-monitor.component.ts
@@ -0,0 +1,59 @@
+/*
+ * 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 { Component, OnInit, ViewChild, Output, EventEmitter, ViewEncapsulation } from '@angular/core';
+import { DICTIONARY } from '../../../../dictionary/global.dictionary';
+
+@Component({
+  selector: 'dlab-ssn-monitor',
+  templateUrl: './ssn-monitor.component.html',
+  styleUrls: ['./ssn-monitor.component.scss'],
+  encapsulation: ViewEncapsulation.None
+})
+export class SsnMonitorComponent implements OnInit {
+  readonly DICTIONARY = DICTIONARY;
+
+  public errorMessage: string = '';
+  public monitorData = {};
+
+  @ViewChild('bindDialog') bindDialog;
+  @Output() manageEnv: EventEmitter<{}> = new EventEmitter();
+
+  ngOnInit() {}
+
+  public open(param, data): void {
+    this.monitorData = data || {};
+    this.bindDialog.open(param);
+  }
+  public close(param, data): void {
+    this.bindDialog.close();
+  }
+
+  public isEmpty(obj) {
+    if (obj) return Object.keys(obj).length === 0;
+  }
+
+  public convertSize(bytes) {
+    if (Number(bytes) === 0) return '0 Byte';
+
+    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
+    const i = Math.floor(Math.log(bytes) / Math.log(1024));
+    return parseFloat((bytes / Math.pow(1024, i)).toFixed(3)) + ' ' + sizes[i];
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/project/index.ts b/services/self-service/src/main/resources/webapp/src/app/administration/project/index.ts
new file mode 100644
index 0000000..6b8e0f4
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/project/index.ts
@@ -0,0 +1,39 @@
+/*
+ * 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 { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+import { MaterialModule } from '../../shared/material.module';
+import { FormControlsModule } from '../../shared/form-controls';
+
+import { ProjectComponent } from './project.component';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    MaterialModule,
+    FormControlsModule,
+  ],
+  declarations: [ProjectComponent],
+  exports: [ProjectComponent]
+})
+export class ProjectModule { }
\ No newline at end of file
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/project/project.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/project/project.component.html
new file mode 100644
index 0000000..4d4b0d8
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/project/project.component.html
@@ -0,0 +1,19 @@
+<!--
+  ~ 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.
+  -->
+  
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/project/project.component.scss b/services/self-service/src/main/resources/webapp/src/app/administration/project/project.component.scss
new file mode 100644
index 0000000..7220975
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/project/project.component.scss
@@ -0,0 +1,18 @@
+/*
+ * 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.
+ */
\ No newline at end of file
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/project/project.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/project/project.component.ts
new file mode 100644
index 0000000..75c222f
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/project/project.component.ts
@@ -0,0 +1,34 @@
+/*
+ * 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 { Component, OnInit } from '@angular/core';
+
+@Component({
+  selector: 'dlab-project',
+  templateUrl: './project.component.html',
+  styleUrls: ['./project.component.scss']
+})
+export class ProjectComponent implements OnInit {
+
+  constructor() { }
+
+  ngOnInit() {
+  }
+
+}


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