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

[incubator-datalab] branch develop updated: [DATALAB-2163]: Merge odahu (UI part) to develop (#997)

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

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


The following commit(s) were added to refs/heads/develop by this push:
     new ce468db  [DATALAB-2163]: Merge odahu (UI part) to develop (#997)
ce468db is described below

commit ce468db6024e19ea176035340cc3de72820db88b
Author: Dmytro Gnatyshyn <42...@users.noreply.github.com>
AuthorDate: Thu Dec 3 16:07:20 2020 +0200

    [DATALAB-2163]: Merge odahu (UI part) to develop (#997)
    
    [DATALAB-2163]: Merge odahu (UI part) to develop
---
 .../app/administration/administration.module.ts    |   5 +-
 .../management-grid/management-grid.component.html |   2 +-
 .../create-odahu-cluster.component.html            |  93 +++++++++++++++++++
 .../create-odahu-cluster.component.scss            |   7 ++
 .../create-odahu-cluster.component.ts              | 103 +++++++++++++++++++++
 .../create-odahu-claster/index.ts}                 |  27 ++++--
 .../webapp/src/app/administration/odahu/index.ts   |  50 ++++++++++
 .../app/administration/odahu/odahu-data.service.ts |  27 ++++++
 .../odahu/odahu-grid/odahu-grid.component.html     |  94 +++++++++++++++++++
 .../odahu/odahu-grid/odahu-grid.component.scss     |  77 +++++++++++++++
 .../odahu/odahu-grid/odahu-grid.component.ts       |  48 ++++++++++
 .../app/administration/odahu/odahu.component.html  |  42 +++++++++
 .../app/administration/odahu/odahu.component.scss} |   0
 .../app/administration/odahu/odahu.component.ts    |  59 ++++++++++++
 .../resources/webapp/src/app/app.routing.module.ts |   8 +-
 .../resources/webapp/src/app/core/core.module.ts   |   3 +-
 .../services/applicationServiceFacade.service.ts   |  20 ++++
 .../webapp/src/app/core/services/index.ts          |   1 +
 .../app/core/services/odahu-deployment.service.ts  |  37 ++++++++
 .../webapp/src/app/core/util/errorUtils.ts         |   2 +-
 .../detail-dialog/detail-dialog.component.html     |  35 ++++++-
 .../detail-dialog/detail-dialog.component.scss     |  27 +++++-
 .../detail-dialog/detail-dialog.component.ts       |  10 +-
 .../resources-grid/resources-grid.component.html   |  30 +++++-
 .../resources-grid/resources-grid.component.scss   |  13 +++
 .../resources-grid/resources-grid.component.ts     |  23 ++++-
 .../resources-grid/resources-grid.model.ts         |  86 +++++++++++++++++
 .../modal-dialog/odahu-action-dialog/index.ts      |  15 +++
 .../odahu-action-dialog.component.ts               |  50 ++++++++++
 .../src/app/shared/navbar/navbar.component.html    |   5 +
 30 files changed, 977 insertions(+), 22 deletions(-)

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
index e535bda..6d5b6d0 100644
--- 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
@@ -23,10 +23,11 @@ import { CommonModule } from '@angular/common';
 import { ManagenementModule } from './management';
 import { ProjectModule } from './project';
 import { RolesModule } from './roles';
+import {OdahuModule} from './odahu';
 
 @NgModule({
-  imports: [CommonModule, ManagenementModule, ProjectModule, RolesModule],
+  imports: [CommonModule, ManagenementModule, ProjectModule, RolesModule, OdahuModule],
   declarations: [],
-  exports: [ManagenementModule, ProjectModule, RolesModule]
+  exports: [ManagenementModule, ProjectModule, RolesModule, OdahuModule]
 })
 export class AdministrationModule { }
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
index 523cd59..4be7e7d 100644
--- 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
@@ -188,7 +188,7 @@
         </th>
         <td mat-cell *matCellDef="let element" class="settings actions-col">
           <span [ngClass]="{'not-allow' : selected?.length}">
-            <span #settings class="actions" (click)="actions.toggle($event, settings)" *ngIf="element.type !== 'edge node'"
+            <span #settings class="actions" (click)="actions.toggle($event, settings)" *ngIf="element.type !== 'edge node' && element.type !== 'odahu'"
               [ngClass]="{
                 'disabled' : (element.status !== 'running' && element.status !== 'stopped' && element.status !== 'stopping' && element.status !== 'failed')
                  || selected?.length || inProgress(element.resources)}">
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/odahu/create-odahu-claster/create-odahu-cluster.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/odahu/create-odahu-claster/create-odahu-cluster.component.html
new file mode 100644
index 0000000..836e630
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/odahu/create-odahu-claster/create-odahu-cluster.component.html
@@ -0,0 +1,93 @@
+<div class="create-odahu-cluster" id="dialog-box">
+  <header class="dialog-header">
+    <h4 class="modal-title">Create Odahu cluster</h4>
+    <button type="button" class="close" (click)="dialogRef.close()">&times;</button>
+  </header>
+  <div class="dialog-content selection">
+    <div id="scrolling" class="content-box mat-reset scrolling-content">
+      <form [formGroup]="createOdahuForm" *ngIf="createOdahuForm" novalidate>
+        <div class="control-group">
+          <label class="label">Select project</label>
+          <div class="control selector-wrapper">
+            <mat-form-field>
+              <mat-label>Select project</mat-label>
+              <mat-select formControlName="project" panelClass="create-resources-dialog">
+                <mat-option *ngFor="let project of projects" [value]="project.name" (click)="setEndpoints(project)">
+                  {{ project.name }}</mat-option>
+                <mat-option *ngIf="!projects.length" class="multiple-select ml-10" disabled>
+                  No projects for creating Odahu clusters
+                </mat-option>
+              </mat-select>
+              <button class="caret">
+                <i class="material-icons">keyboard_arrow_down</i>
+              </button>
+            </mat-form-field>
+          </div>
+        </div>
+
+        <div class="control-group">
+          <label class="label">Select endpoint</label>
+          <div class="control selector-wrapper" [ngClass]="{ 'not-active' : !endpoints.length }">
+            <mat-form-field>
+              <mat-label>Select endpoint</mat-label>
+              <mat-select formControlName="endpoint" disableOptionCentering [disabled]="!endpoints.length"
+                          panelClass="create-resources-dialog">
+                <mat-option *ngFor="let endpoint of endpoints" [value]="endpoint">
+                  {{ endpoint }}
+                </mat-option>
+                <mat-option *ngIf="!endpoints.length" class="multiple-select ml-10" disabled>Endpoints list is empty</mat-option>
+              </mat-select>
+              <button class="caret">
+                <i class="material-icons">keyboard_arrow_down</i>
+              </button>
+            </mat-form-field>
+          </div>
+        </div>
+
+        <div class="control-group name-control">
+          <label class="label">Name</label>
+          <div class="control">
+            <input type="text" class="form-control" placeholder="Enter Name" formControlName="name">
+            <span class="error" *ngIf="!createOdahuForm.controls.name.valid && createOdahuForm.controls.name.dirty && !createOdahuForm.controls.name.hasError('duplication')">
+              Odahu cluster name can only contain letters and numbers
+            </span>
+            <span class="error" *ngIf="createOdahuForm.controls.name.hasError('duplication')">This Odahu cluster name already exists.</span>
+          </div>
+        </div>
+
+        <div class="control-group name-control">
+          <label class="label">Custom tag</label>
+          <div class="control">
+            <input type="text" class="form-control" placeholder="Enter custom tag" formControlName="custom_tag">
+          <span class="error"
+            *ngIf="!createOdahuForm.controls.custom_tag.valid && createOdahuForm.controls.custom_tag.dirty">
+            Custom tag can only contain letters, numbers, hyphens and '_' but can not end with special characters</span>
+          </div>
+        </div>
+
+<!--        <div class="control-group">-->
+<!--          <label class="label" [ngStyle]="!createLegionClusterForm.controls.useExistingClusterUrl.value && {'width': '100%' }">-->
+<!--            <input  type="checkbox" formControlName="useExistingClusterUrl"/> Use existing k8s cluster-->
+<!--          </label>-->
+<!--          <div class="control" *ngIf="createLegionClusterForm.controls.useExistingClusterUrl.value">-->
+<!--            <input type="text" class="form-control"-->
+<!--                                   formControlName="existingClusterUrl" placeholder="Enter ODAHU k8s cluster URL"/>-->
+<!--            <span class="error url-error">-->
+<!--                  <span *ngIf="!createLegionClusterForm.controls.existingClusterUrl.valid">Please enter valid cluster URL</span>-->
+<!--                </span>-->
+<!--          </div>-->
+<!--        </div>-->
+
+        <div class="text-center m-top-30">
+          <button mat-raised-button type="button" class="butt action" (click)="dialogRef.close()">Cancel</button>
+          <button mat-raised-button type="button" class="butt butt-success action"
+                  [disabled]="!createOdahuForm.valid" (click)="createOdahuCluster(createOdahuForm.value)">
+            Create
+          </button>
+        </div>
+
+      </form>
+    </div>
+  </div>
+</div>
+
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/odahu/create-odahu-claster/create-odahu-cluster.component.scss b/services/self-service/src/main/resources/webapp/src/app/administration/odahu/create-odahu-claster/create-odahu-cluster.component.scss
new file mode 100644
index 0000000..51dcf57
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/odahu/create-odahu-claster/create-odahu-cluster.component.scss
@@ -0,0 +1,7 @@
+.create-odahu-cluster{
+  .error{
+    position: absolute;
+    left: 0;
+    top: 36px;
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/odahu/create-odahu-claster/create-odahu-cluster.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/odahu/create-odahu-claster/create-odahu-cluster.component.ts
new file mode 100644
index 0000000..20e84b4
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/odahu/create-odahu-claster/create-odahu-cluster.component.ts
@@ -0,0 +1,103 @@
+/*
+ * 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, Inject } from '@angular/core';
+import { FormGroup, FormBuilder, Validators } from '@angular/forms';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { ToastrService } from 'ngx-toastr';
+
+import { Project } from '../../project/project.component';
+import { ProjectService, OdahuDeploymentService } from '../../../core/services';
+
+import { DICTIONARY } from '../../../../dictionary/global.dictionary';
+import {CheckUtils, PATTERNS} from '../../../core/util';
+
+
+@Component({
+  selector: 'create-odahu-cluster',
+  templateUrl: 'create-odahu-cluster.component.html',
+  styleUrls: ['./create-odahu-cluster.component.scss']
+})
+
+export class CreateOdahuClusterComponent implements OnInit {
+  readonly DICTIONARY = DICTIONARY;
+  public createOdahuForm: FormGroup;
+
+  projects: Project[] = [];
+  endpoints: Array<String> = [];
+
+  constructor(
+    @Inject(MAT_DIALOG_DATA) public data: any,
+    public toastr: ToastrService,
+    public dialogRef: MatDialogRef<CreateOdahuClusterComponent>,
+    private _fb: FormBuilder,
+    private projectService: ProjectService,
+    private odahuDeploymentService: OdahuDeploymentService,
+  ) {
+  }
+
+  ngOnInit() {
+    this.getUserProjects();
+    this.initFormModel();
+  }
+
+  public getUserProjects(): void {
+    this.projectService.getUserProjectsList(true).subscribe((projects: any) => {
+      this.projects = projects.filter(project => {
+        return project.endpoints.length > project.odahu.filter(od => od.status !== 'FAILED' && od.status !== 'TERMINATED').length; }
+        );
+    });
+  }
+
+  public setEndpoints(project): void {
+    this.endpoints = project.endpoints
+      .filter(e => e.status === 'RUNNING' && !this.data.some(odahu => odahu.status !== 'FAILED'
+        && odahu.status !== 'TERMINATED'
+        && odahu.endpoint === e.name
+        && odahu.project === project.name)
+      )
+      .map(e => e.name);
+  }
+
+  private initFormModel(): void {
+    this.createOdahuForm = this._fb.group({
+      name: ['', [Validators.required, Validators.pattern(PATTERNS.namePattern), this.checkDuplication.bind(this)]],
+      project: ['', Validators.required],
+      endpoint: ['', [Validators.required]],
+      custom_tag: ['', [Validators.pattern(PATTERNS.namePattern)]]
+    });
+  }
+
+  private createOdahuCluster(value): void {
+    this.dialogRef.close();
+    this.odahuDeploymentService.createOdahuNewCluster(value).subscribe(() => {
+      this.toastr.success('Odahu cluster creation is processing!', 'Success!');
+      }, error => this.toastr.error(error.message || 'Odahu cluster creation failed!', 'Oops!')
+    );
+  }
+
+  private checkDuplication(control) {
+    if (control && control.value) {
+      for (let index = 0; index < this.data.length; index++) {
+        if (CheckUtils.delimitersFiltering(control.value) === CheckUtils.delimitersFiltering(this.data[index].name))
+          return { duplication: true };
+      }
+    }
+  }
+}
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/odahu/create-odahu-claster/index.ts
similarity index 54%
copy from services/self-service/src/main/resources/webapp/src/app/administration/administration.module.ts
copy to services/self-service/src/main/resources/webapp/src/app/administration/odahu/create-odahu-claster/index.ts
index e535bda..bd465fa 100644
--- 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/odahu/create-odahu-claster/index.ts
@@ -19,14 +19,27 @@
 
 import { NgModule } from '@angular/core';
 import { CommonModule } from '@angular/common';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 
-import { ManagenementModule } from './management';
-import { ProjectModule } from './project';
-import { RolesModule } from './roles';
+import { MaterialModule } from '../../../shared/material.module';
+import { FormControlsModule } from '../../../shared/form-controls';
+import { KeysPipeModule, UnderscorelessPipeModule } from '../../../core/pipes';
+import {CreateOdahuClusterComponent} from './create-odahu-cluster.component';
+
+export * from './create-odahu-cluster.component';
 
 @NgModule({
-  imports: [CommonModule, ManagenementModule, ProjectModule, RolesModule],
-  declarations: [],
-  exports: [ManagenementModule, ProjectModule, RolesModule]
+  imports: [
+    CommonModule,
+    FormsModule,
+    ReactiveFormsModule,
+    FormControlsModule,
+    MaterialModule,
+    KeysPipeModule,
+    UnderscorelessPipeModule,
+  ],
+  declarations: [CreateOdahuClusterComponent],
+  entryComponents: [CreateOdahuClusterComponent],
+  exports: [CreateOdahuClusterComponent]
 })
-export class AdministrationModule { }
+export class CreateOdahuClusterModule { }
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/odahu/index.ts b/services/self-service/src/main/resources/webapp/src/app/administration/odahu/index.ts
new file mode 100644
index 0000000..1564326
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/odahu/index.ts
@@ -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.
+ */
+
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+
+import { MaterialModule } from '../../shared/material.module';
+import { FormControlsModule } from '../../shared/form-controls';
+import { UnderscorelessPipeModule } from '../../core/pipes/underscoreless-pipe';
+
+import {BubbleModule} from '../../shared/bubble';
+import {OdahuComponent} from './odahu.component';
+import {OdahuDataService} from './odahu-data.service';
+import {OdahuGridComponent} from './odahu-grid/odahu-grid.component';
+import {CreateOdahuClusterModule} from './create-odahu-claster';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    ReactiveFormsModule,
+    MaterialModule,
+    FormControlsModule,
+    UnderscorelessPipeModule,
+    BubbleModule,
+    CreateOdahuClusterModule
+  ],
+  declarations: [OdahuComponent, OdahuGridComponent],
+  entryComponents: [],
+  providers: [OdahuDataService],
+  exports: [OdahuComponent]
+})
+export class OdahuModule { }
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/odahu/odahu-data.service.ts b/services/self-service/src/main/resources/webapp/src/app/administration/odahu/odahu-data.service.ts
new file mode 100644
index 0000000..d8a30a6
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/odahu/odahu-data.service.ts
@@ -0,0 +1,27 @@
+import { Injectable } from '@angular/core';
+import {BehaviorSubject, Observable} from 'rxjs';
+import {OdahuDeploymentService} from '../../core/services';
+
+
+@Injectable({
+  providedIn: 'root'
+})
+
+export class OdahuDataService {
+  _odahuClasters = new BehaviorSubject<any>(null);
+
+  constructor(private odahuDeploymentService: OdahuDeploymentService) {
+    this.getClastersList();
+  }
+
+  public updateClasters(): void {
+    this.getClastersList();
+  }
+
+  private getClastersList(): void {
+   this.odahuDeploymentService.getOduhuClustersList().subscribe(
+      (response: any ) => {
+        return this._odahuClasters.next(response);
+      });
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/odahu/odahu-grid/odahu-grid.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/odahu/odahu-grid/odahu-grid.component.html
new file mode 100644
index 0000000..73d7952
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/odahu/odahu-grid/odahu-grid.component.html
@@ -0,0 +1,94 @@
+
+<table mat-table [dataSource]="dataSource" class="odahu-clasters-table mat-elevation-z6 selection">
+  <ng-container matColumnDef="project">
+    <th mat-header-cell *matHeaderCellDef class="project"> Project name </th>
+    <td mat-cell *matCellDef="let element" class="project">
+      <span *ngIf="element && element.project">{{element.project}}</span>
+    </td>
+    <td mat-footer-cell *matFooterCellDef></td>
+  </ng-container>
+
+  <ng-container matColumnDef="endpoint-url">
+    <th mat-header-cell *matHeaderCellDef class="endpoint"> Endpoint </th>
+    <td mat-cell *matCellDef="let element" class="endpoint">
+      <span *ngIf="element && element.endpoint" matTooltip="{{element.endpoint}}" [matTooltipPosition]="'above'">
+        {{element.endpoint}}
+      </span>
+    </td>
+    <td mat-footer-cell *matFooterCellDef></td>
+  </ng-container>
+
+  <ng-container matColumnDef="odahu-name">
+    <th mat-header-cell *matHeaderCellDef class="odahu-name"> Odahu cluster name </th>
+    <td mat-cell *matCellDef="let element" class="odahu-name">
+      <span *ngIf="element && element.name">{{element.name}}</span>
+    </td>
+    <td mat-footer-cell *matFooterCellDef></td>
+  </ng-container>
+
+  <ng-container matColumnDef="odahu-status">
+    <th mat-header-cell *matHeaderCellDef class="odahu-status"> Odahu cluster status </th>
+    <td mat-cell *matCellDef="let element" class="odahu-status">
+      <span *ngIf="element && element.name" [ngClass]="element.status.toLowerCase()">{{ element.status | titlecase}}</span>
+    </td>
+    <td mat-footer-cell *matFooterCellDef></td>
+  </ng-container>
+
+  <ng-container matColumnDef="actions">
+    <th mat-header-cell *matHeaderCellDef class="odahu-actions"></th>
+    <td mat-cell *matCellDef="let element" class="settings">
+      <span *ngIf="element && element.name" #settings (click)="actions.toggle($event, settings)" class="actions" [ngClass]="{'disabled': element.status !== 'RUNNING' && element.status !== 'STOPPED'}"></span>
+      <bubble-up #actions class="list-menu" position="bottom-left" alternative="top-left">
+        <ul class="list-unstyled">
+          <div class="active-items"></div>
+          <li class="project-seting-item" *ngIf="element.status === 'STOPPED'" (click)="odahuAction(element, 'start')">
+            <i class="material-icons">play_circle_outline</i>
+            <a class="action">
+              Start
+            </a>
+          </li>
+          <li class="project-seting-item" *ngIf="element.status === 'RUNNING'" (click)="odahuAction(element, 'stop')">
+            <i class="material-icons">pause_circle_outline</i>
+            <a class="action" >
+              Stop
+            </a>
+          </li>
+          <li class="project-seting-item" [ngClass]="{'disabled' : element.status === 'STOPPED'}" *ngIf="element.status !== 'TERMINATED' && element.status !== 'TERMINATING'" (click)="odahuAction(element, 'terminate')">
+            <i class="material-icons">phonelink_off</i>
+            <a class="action">
+              Terminate
+            </a>
+          </li>
+          <!--<li class="project-seting-item">-->
+            <!--<i class="material-icons">arrow_downward</i>-->
+            <!--<a>-->
+              <!--Scale-down-->
+            <!--</a>-->
+          <!--</li>-->
+          <!--<li class="project-seting-item">-->
+            <!--<i class="material-icons">arrow_upward</i>-->
+            <!--<a  class="action">-->
+              <!--Scale-up-->
+            <!--</a>-->
+          <!--</li>-->
+        </ul>
+      </bubble-up>
+    </td>
+    <td mat-footer-cell *matFooterCellDef></td>
+  </ng-container>
+
+    <ng-container matColumnDef="placeholder">
+      <td mat-footer-cell *matFooterCellDef
+          [colSpan]="displayedColumns.length"
+          class="info">
+          <span>No Odahu clusters</span>
+      </td>
+      <td mat-footer-cell *matFooterCellDef></td>
+    </ng-container>
+
+  <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
+  <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
+  <tr [hidden]="odahuList && odahuList.length" mat-footer-row *matFooterRowDef="['placeholder']"></tr>
+</table>
+
+
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/odahu/odahu-grid/odahu-grid.component.scss b/services/self-service/src/main/resources/webapp/src/app/administration/odahu/odahu-grid/odahu-grid.component.scss
new file mode 100644
index 0000000..62e6200
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/odahu/odahu-grid/odahu-grid.component.scss
@@ -0,0 +1,77 @@
+.odahu-clasters-table {
+  width: 100%;
+
+  .project, .endpoint, .odahu-name, .odahu-status{
+    width: 23%;
+  }
+
+  td.settings {
+    position: relative;
+    vertical-align: middle !important;
+    text-align: right;
+  }
+
+  .list-menu {
+    min-width: 190px;
+  }
+
+  .material-icons {
+    font-size: 18px;
+    padding-top: 1px;
+  }
+
+  td.settings .actions {
+    background-image: url(../../../../assets/svg/settings_icon.svg);
+    width: 16px;
+    height: 16px;
+    display: inline-block;
+    text-align: center;
+    cursor: pointer;
+    &.disabled {
+      opacity: 0.4;
+      cursor: not-allowed;
+      pointer-events: none;
+    }
+  }
+
+  td {
+    &.info.mat-footer-cell{
+      text-align: center;
+      padding: 40px;
+    }
+    .settings {
+      position: relative;
+      vertical-align: middle !important;
+      text-align: right;
+
+      .actions {
+        background-image: url(../../../../assets/svg/settings_icon.svg);
+        width: 16px;
+        height: 16px;
+        display: inline-block;
+        text-align: center;
+        cursor: pointer;
+      }
+    }
+
+    .project-seting-item {
+      display: flex;
+      padding: 10px;
+      align-items: center;
+      border-bottom: 1px solid #edf1f5;
+      cursor: pointer;
+      color: #577289;
+
+      &:hover {
+        color: #36afd5;
+        transition: all .45s ease-in-out;
+      }
+
+      a {
+        padding-left: 5px;
+      }
+    }
+  }
+}
+
+
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/odahu/odahu-grid/odahu-grid.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/odahu/odahu-grid/odahu-grid.component.ts
new file mode 100644
index 0000000..817d12b
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/odahu/odahu-grid/odahu-grid.component.ts
@@ -0,0 +1,48 @@
+import { Component, OnInit } from '@angular/core';
+import {Subscription} from 'rxjs';
+import {OdahuDataService} from '../odahu-data.service';
+import { MatTableDataSource } from '@angular/material/table';
+import {OdahuDeploymentService} from '../../../core/services';
+import {ToastrService} from 'ngx-toastr';
+import {MatDialog} from '@angular/material/dialog';
+import {OdahuActionDialogComponent} from '../../../shared/modal-dialog/odahu-action-dialog';
+
+@Component({
+  selector: 'odahu-grid',
+  templateUrl: './odahu-grid.component.html',
+  styleUrls: ['./odahu-grid.component.scss']
+})
+export class OdahuGridComponent implements OnInit {
+  private odahuList: any[];
+  private subscriptions: Subscription = new Subscription();
+  public dataSource: MatTableDataSource<any>;
+  displayedColumns: string[] = [ 'odahu-name', 'project', 'endpoint-url', 'odahu-status', 'actions'];
+
+  constructor(
+    private odahuDataService: OdahuDataService,
+    private odahuDeploymentService: OdahuDeploymentService,
+    public toastr: ToastrService,
+    public dialog: MatDialog
+  ) { }
+
+  ngOnInit() {
+    this.subscriptions.add(this.odahuDataService._odahuClasters.subscribe(
+      (value) => {
+        if (value) {
+          this.odahuList = value;
+          this.dataSource = new MatTableDataSource(value);
+        }
+      }));
+  }
+
+  private odahuAction(element: any, action: string) {
+    this.dialog.open(OdahuActionDialogComponent, {data: {type: action, item: element}, panelClass: 'modal-sm'})
+      .afterClosed().subscribe(result => {
+        result && this.odahuDeploymentService.odahuAction(element,  action).subscribe(v =>
+          this.odahuDataService.updateClasters(),
+          error => this.toastr.error(`Odahu cluster ${action} failed!`, 'Oops!')
+        ) ;
+      }, error => this.toastr.error(error.message || `Odahu cluster ${action} failed!`, 'Oops!')
+    );
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/odahu/odahu.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/odahu/odahu.component.html
new file mode 100644
index 0000000..a59a5d5
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/odahu/odahu.component.html
@@ -0,0 +1,42 @@
+<!--
+  ~ 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="base-retreat">
+  <div class="sub-nav">
+    <div>
+      <button mat-raised-button class="butt butt-create" (click)="createOdahuCluster()">
+        <i class="material-icons">add</i>Create new
+      </button>
+    </div>
+    <div>
+      <button mat-raised-button class="butt" (click)="refreshGrid()">
+        <i class="material-icons">autorenew</i>Refresh
+      </button>
+    </div>
+  </div>
+
+  <mat-divider></mat-divider>
+
+  <div>
+    <odahu-grid>
+    </odahu-grid>
+  </div>
+</div>
+
diff --git a/services/self-service/src/test/java/com/epam/datalab/backendapi/.gitkeep b/services/self-service/src/main/resources/webapp/src/app/administration/odahu/odahu.component.scss
similarity index 100%
rename from services/self-service/src/test/java/com/epam/datalab/backendapi/.gitkeep
rename to services/self-service/src/main/resources/webapp/src/app/administration/odahu/odahu.component.scss
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/odahu/odahu.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/odahu/odahu.component.ts
new file mode 100644
index 0000000..642b311
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/odahu/odahu.component.ts
@@ -0,0 +1,59 @@
+import { Component, OnInit } from '@angular/core';
+import {OdahuDataService} from './odahu-data.service';
+import {Subscription} from 'rxjs';
+import {MatDialog} from '@angular/material/dialog';
+import {ToastrService} from 'ngx-toastr';
+import {CreateOdahuClusterComponent} from './create-odahu-claster/create-odahu-cluster.component';
+import {HealthStatusService, OdahuDeploymentService} from '../../core/services';
+
+export interface OdahuCluster {
+  name: string;
+  project: string;
+  endpoint: string;
+}
+
+@Component({
+  selector: 'datalab-odahu',
+  templateUrl: './odahu.component.html',
+  styleUrls: ['./odahu.component.scss']
+})
+export class OdahuComponent implements OnInit {
+
+  private odahuList: any[];
+  private subscriptions: Subscription = new Subscription();
+  private healthStatus;
+
+  constructor(
+    private odahuDataService: OdahuDataService,
+    private dialog: MatDialog,
+    public toastr: ToastrService,
+    public odahuDeploymentService: OdahuDeploymentService,
+    private healthStatusService: HealthStatusService,
+  ) { }
+
+  ngOnInit() {
+    this.getEnvironmentHealthStatus();
+    this.subscriptions.add(this.odahuDataService._odahuClasters.subscribe(
+      (value) => {
+        if (value) this.odahuList = value;
+      }));
+    this.refreshGrid();
+  }
+
+  public createOdahuCluster(): void {
+    this.dialog.open(CreateOdahuClusterComponent, {  data: this.odahuList, panelClass: 'modal-lg' })
+      .afterClosed().subscribe((result) => {
+      result && this.getEnvironmentHealthStatus();
+      this.refreshGrid();
+      });
+  }
+
+  private getEnvironmentHealthStatus(): void {
+    this.healthStatusService.getEnvironmentHealthStatus()
+      .subscribe((result: any) => this.healthStatus = result);
+  }
+
+  public refreshGrid(): void {
+    this.odahuDataService.updateClasters();
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/app.routing.module.ts b/services/self-service/src/main/resources/webapp/src/app/app.routing.module.ts
index 35c9df7..461c5f7 100644
--- a/services/self-service/src/main/resources/webapp/src/app/app.routing.module.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/app.routing.module.ts
@@ -34,6 +34,7 @@ import { RolesComponent } from './administration/roles/roles.component';
 import { SwaggerComponent } from './swagger/swagger.component';
 import { AuthorizationGuard, CheckParamsGuard, CloudProviderGuard, AdminGuard, AuditGuard } from './core/services';
 import {AuditComponent} from './reports/audit/audit.component';
+import {OdahuComponent} from './administration/odahu/odahu.component';
 
 const routes: Routes = [{
   path: 'login',
@@ -59,7 +60,12 @@ const routes: Routes = [{
       path: 'projects',
       component: ProjectComponent,
       canActivate: [AuthorizationGuard, AdminGuard],
-    }, {
+    },
+     {
+    //   path: 'odahu',
+    //   component: OdahuComponent,
+    //   canActivate: [AuthorizationGuard, AdminGuard],
+    // }, {
       path: 'roles',
       component: RolesComponent,
       canActivate: [AuthorizationGuard, AdminGuard],
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/core.module.ts b/services/self-service/src/main/resources/webapp/src/app/core/core.module.ts
index 4b1c0f9..ae6221a 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/core.module.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/core.module.ts
@@ -48,7 +48,7 @@ import { NoCacheInterceptor } from './interceptors/nocache.interceptor';
 import { ErrorInterceptor } from './interceptors/error.interceptor';
 
 import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
-import {AuditGuard} from './services';
+import {AuditGuard, OdahuDeploymentService} from './services';
 
 @NgModule({
   imports: [CommonModule],
@@ -85,6 +85,7 @@ export class CoreModule {
         ProjectService,
         EndpointService,
         UserAccessKeyService,
+        OdahuDeploymentService,
 
         { provide: MatDialogRef, useValue: {} },
         { provide: MAT_DIALOG_DATA, useValue: [] },
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/services/applicationServiceFacade.service.ts b/services/self-service/src/main/resources/webapp/src/app/core/services/applicationServiceFacade.service.ts
index 30882f1..97bbfd4 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/services/applicationServiceFacade.service.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/services/applicationServiceFacade.service.ts
@@ -75,6 +75,7 @@ export class ApplicationServiceFacade {
   private static readonly DOWNLOAD_REPORT = 'download_report';
   private static readonly SETTINGS = 'settings';
   private static readonly PROJECT = 'project';
+  private static readonly ODAHU = 'odahu';
   private static readonly ENDPOINT = 'endpoint';
   private static readonly ENDPOINT_CONNECTION = 'endpoint_connection';
   private static readonly AUDIT = 'audit';
@@ -654,6 +655,24 @@ export class ApplicationServiceFacade {
       data);
   }
 
+  public createOdahuCluster(data): Observable<any> {
+    return this.buildRequest(HTTPMethod.POST,
+      this.requestRegistry.Item(ApplicationServiceFacade.ODAHU),
+      data);
+  }
+
+  public getOdahuList(): Observable<any> {
+    return this.buildRequest(HTTPMethod.GET,
+      this.requestRegistry.Item(ApplicationServiceFacade.ODAHU),
+      null);
+  }
+
+  public odahuStartStop(data, params): Observable<any> {
+    return this.buildRequest(HTTPMethod.POST,
+      this.requestRegistry.Item(ApplicationServiceFacade.ODAHU) + `/${params}`,
+      data);
+  }
+
   private setupRegistry(): void {
     this.requestRegistry = new Dictionary<string>();
 
@@ -759,4 +778,5 @@ export class ApplicationServiceFacade {
       return this.http.get(body ? (url + body) : url, opt);
     }
   }
+
 }
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/services/index.ts b/services/self-service/src/main/resources/webapp/src/app/core/services/index.ts
index 2005741..4e9a898 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/services/index.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/services/index.ts
@@ -39,3 +39,4 @@ export * from './dataengineConfiguration.service';
 export * from './storage.service';
 export * from './project.service';
 export * from './endpoint.service';
+export * from './odahu-deployment.service';
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/services/odahu-deployment.service.ts b/services/self-service/src/main/resources/webapp/src/app/core/services/odahu-deployment.service.ts
new file mode 100644
index 0000000..f6fe528
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/core/services/odahu-deployment.service.ts
@@ -0,0 +1,37 @@
+import { Injectable } from '@angular/core';
+import {Observable} from 'rxjs';
+import { map, catchError } from 'rxjs/operators';
+import { ApplicationServiceFacade } from './applicationServiceFacade.service';
+import { ErrorUtils } from '../util';
+
+@Injectable({
+  providedIn: 'root'
+})
+
+export class OdahuDeploymentService {
+  constructor(private applicationServiceFacade: ApplicationServiceFacade) { }
+
+  public createOdahuNewCluster(data): Observable<{}> {
+    return this.applicationServiceFacade
+      .createOdahuCluster(data)
+      .pipe(
+        map(response => response),
+        catchError(ErrorUtils.handleServiceError));
+  }
+
+  public getOduhuClustersList(): Observable<{}> {
+    return this.applicationServiceFacade
+      .getOdahuList()
+      .pipe(
+        map(response => response),
+        catchError(ErrorUtils.handleServiceError));
+  }
+
+  public odahuAction(data, action) {
+    return this.applicationServiceFacade
+        .odahuStartStop(data, action)
+        .pipe(
+            map(response => response),
+            catchError(ErrorUtils.handleServiceError));
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/util/errorUtils.ts b/services/self-service/src/main/resources/webapp/src/app/core/util/errorUtils.ts
index c3c9179..5d25db2 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/util/errorUtils.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/util/errorUtils.ts
@@ -20,6 +20,7 @@
 
 import { throwError as observableThrowError } from 'rxjs';
 import { CheckUtils } from './checkUtils';
+import {logger} from 'codelyzer/util/logger';
 
 export class ErrorUtils {
 
@@ -41,7 +42,6 @@ export class ErrorUtils {
 
   public static handleServiceError(errorMessage) {
     const error = CheckUtils.isJSON(errorMessage.error) ? JSON.parse(errorMessage.error) : errorMessage.error;
-
     return observableThrowError({
       status: error.code,
       statusText: errorMessage.statusText,
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.html
index d73dae3..5d297e6 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.html
@@ -23,7 +23,20 @@
     <button type="button" class="close" (click)="dialogRef.close()">&times;</button>
   </header>
   <div class="dialog-content">
-    <div *ngIf="data ">
+    <div *ngIf="data.legion">
+      <table class="detail-header">
+        <tr>
+          <td>{{odahu.name}}</td>
+          <td>
+            <span class="status" ngClass="{{odahu.status || ''}}">
+              {{odahu.status}}
+            </span>
+          </td>
+          <td>{{odahu.shape}}</td>
+        </tr>
+      </table>
+    </div>
+    <div *ngIf="data.notebook">
       <table class="detail-header">
         <tr>
           <td>{{notebook.template_name || notebook.name}}</td>
@@ -258,5 +271,25 @@
       </div>
     </div>
 
+    <div class="legion-info" *ngIf="data.odahu">
+      <div class="content-box">
+        <div class="detail-info" *ngIf="!odahu.error_message">
+        <div  class="links_block">
+          <div *ngFor="let url of odahu.url" class="odahu-links">
+            <div class="odahu-link">
+              <span class="description">{{url.description }}: &nbsp;</span>
+              <a class="ellipsis" matTooltip="{{ url.url}}" matTooltipPosition="above" href="{{ url.url}}"
+                 target="_blank">{{ url.url}}
+              </a>
+            </div>
+            <div class="grafana" *ngIf="url.description === 'Grafana'">
+              <div><span>Gafana user:&nbsp;</span><span class="creds">{{odahu.bucket_name}}</span></div>
+              <div><span>Gafana password:&nbsp;</span><span class="creds">{{odahu.shared_bucket_name}}</span></div>
+            </div>
+          </div>
+        </div>
+    </div>
+      </div>
+    </div>
   </div>
 </div>
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.scss b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.scss
index e91c04b..fed1aa5 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.scss
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.scss
@@ -27,8 +27,31 @@
 
 .links_block {
 
-  >p {
-    display: flex;
+  .odahu-links:not(:last-child){
+    margin-bottom: 20px;
+  }
+
+  .grafana{
+    white-space: nowrap;
+    padding-left: 30px;
+    color: $blue-grey-color;
+
+    div{
+      display: flex;
+
+      span{
+        font-size: 13px;
+        font-weight: 500;
+
+        &.creds{
+          font-weight: 600;
+        }
+      }
+    }
+  }
+
+  >p, .odahu-link {
+     display: flex;
 
     .description {
       white-space: nowrap;
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.ts
index dfda98c..cd1b574 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.ts
@@ -47,7 +47,7 @@ export class DetailDialogComponent implements OnInit {
   bucketStatus: object = {};
   isBucketAllowed = true;
   isCopyIconVissible: {bucket} = {bucket: false};
-
+  public odahu: any;
   public configurationForm: FormGroup;
   @ViewChild('configurationNode') configuration;
 
@@ -61,7 +61,15 @@ export class DetailDialogComponent implements OnInit {
     public toastr: ToastrService,
     public auditService: AuditService
   ) {
+    if (data.notebook) {
+      this.notebook = data.notebook;
+      this.PROVIDER = this.data.notebook.cloud_provider;
+    }
 
+    if (data.odahu) {
+      this.odahu = data.odahu;
+      this.PROVIDER = this.data.odahu.cloud_provider || 'azure';
+    }
   }
 
   ngOnInit() {
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.component.html
index 981d56e..2124f24 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.component.html
@@ -115,8 +115,9 @@
 <!--        [@detailExpand]="element == expandedElement ? 'expanded' : 'collapsed'" -->
 
         <tr *ngFor="let element of element.exploratory; let i = index" class="element-row mat-row">
-          <td class="name-col highlight" (click)="printDetailEnvironmentModal(element)">
-            <span matTooltip="{{ element.name }}" matTooltipPosition="above">{{ element.name }}</span>
+          <td class="name-col">
+            <span *ngIf="element.shape !== 'odahu cluster'" matTooltip="{{ element.name }}" matTooltipPosition="above" (click)="printDetailEnvironmentModal(element)">{{ element.name }}</span>
+            <span *ngIf="element.shape === 'odahu cluster'" matTooltip="{{ element.name }}" matTooltipPosition="above" (click)="printDetailOdahuModal(element)">{{ element.name }}</span>
           </td>
           <td class="status-col status" ngClass="{{ element.status.toLowerCase() || ''}}">
             {{element.status | underscoreless }}
@@ -162,7 +163,7 @@
             </span>
 
             <bubble-up #actions class="list-menu" position="bottom-left" alternative="top-left">
-              <ul class="list-unstyled">
+              <ul class="list-unstyled" *ngIf="element.shape !== 'odahu cluster'">
                 <div class="active-items" *ngIf="element.status.toLowerCase() !== 'failed'
                 && element.status !== 'terminating'
                 && element.status !== 'terminated'
@@ -248,6 +249,29 @@
                   </div>
                 </li>
               </ul>
+              <ul class="list-unstyled" *ngIf="element.shape === 'odahu cluster'">
+                <li class="project-seting-item" *ngIf="element.status === 'stopped'" (click)="odahuAction(element, 'start')">
+                  <i class="material-icons">play_circle_outline</i>
+                  <a class="action">
+                    Start
+                  </a>
+                </li>
+                <li class="project-seting-item" *ngIf="element.status === 'running'" (click)="odahuAction(element, 'stop')">
+                  <i class="material-icons">pause_circle_outline</i>
+                  <a class="action" >
+                    Stop
+                  </a>
+                </li>
+                <li class="project-seting-item"
+                    [ngClass]="{'disabled': element.status === 'stopped' || element.status.toLowerCase() === 'stopping' || element.status.toLowerCase() === 'starting'}"
+                    *ngIf="element.status === element.status !== 'terminated' && element.status !== 'terminating'" (click)="odahuAction(element, 'terminate')"
+                >
+                  <i class="material-icons">phonelink_off</i>
+                  <a class="action">
+                    Terminate
+                  </a>
+                </li>
+              </ul>
             </bubble-up>
           </td>
         </tr>
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.component.scss b/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.component.scss
index 03df6ff..6ef7e50 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.component.scss
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.component.scss
@@ -441,3 +441,16 @@ table.resources {
 
 
 
+.info {
+  padding: 40px;
+  text-align: center;
+}
+
+.odahu-icon{
+  vertical-align: middle;
+  margin-left: 10px;
+}
+
+.content-row{
+  background-clip: padding-box;
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.component.ts
index 7bae81b..cdb4407 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.component.ts
@@ -22,7 +22,7 @@ import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
 import { animate, state, style, transition, trigger } from '@angular/animations';
 import { ToastrService } from 'ngx-toastr';
 import { MatDialog } from '@angular/material/dialog';
-import {ProjectService, UserResourceService} from '../../core/services';
+import {ProjectService, UserResourceService, OdahuDeploymentService} from '../../core/services';
 import { ExploratoryModel, Exploratory } from './resources-grid.model';
 import { FilterConfigurationModel } from './filter-configuration.model';
 import { GeneralEnvironmentStatus } from '../../administration/management/management.model';
@@ -37,6 +37,7 @@ import { ConfirmationDialogComponent } from '../../shared/modal-dialog/confirmat
 import { SchedulerComponent } from '../scheduler';
 import { DICTIONARY } from '../../../dictionary/global.dictionary';
 import {ProgressBarService} from '../../core/services/progress-bar.service';
+import {OdahuActionDialogComponent} from '../../shared/modal-dialog/odahu-action-dialog';
 import {ComputationModel} from '../computational/computational-resource.model';
 import {NotebookModel} from '../exploratory/notebook.model';
 import {AuditService} from '../../core/services/audit.service';
@@ -132,6 +133,7 @@ export class ResourcesGridComponent implements OnInit {
     private progressBarService: ProgressBarService,
     private projectService: ProjectService,
     private auditService: AuditService,
+    private odahuDeploymentService: OdahuDeploymentService,
   ) { }
 
   ngOnInit(): void {
@@ -227,6 +229,11 @@ export class ResourcesGridComponent implements OnInit {
       .afterClosed().subscribe(() => this.buildGrid());
   }
 
+  public printDetailOdahuModal(data): void {
+    this.dialog.open(DetailDialogComponent, { data: {odahu: data}, panelClass: 'modal-lg' })
+      .afterClosed().subscribe(() => this.buildGrid());
+  }
+
   public printCostDetails(data): void {
     this.dialog.open(CostDetailsDialogComponent, { data: data, panelClass: 'modal-xl' })
       .afterClosed().subscribe(() => this.buildGrid());
@@ -247,7 +254,8 @@ export class ResourcesGridComponent implements OnInit {
           error => this.toastr.error(error.message || 'Exploratory starting failed!', 'Oops!'));
     } else if (action === 'stop') {
       const compute =  data.resources.filter(cluster => cluster.status === 'running');
-      this.dialog.open(ConfirmationDialogComponent, { data: { notebook: data, compute, type: ConfirmationDialogType.StopExploratory }, panelClass: 'modal-sm' })
+      this.dialog.open(ConfirmationDialogComponent,
+        { data: { notebook: data, compute, type: ConfirmationDialogType.StopExploratory }, panelClass: 'modal-sm' })
         .afterClosed().subscribe((res) => {
         res && this.buildGrid();
       });
@@ -445,6 +453,17 @@ export class ResourcesGridComponent implements OnInit {
         (error) => console.log('UPDATE USER PREFERENCES ERROR ', error));
   }
 
+  private odahuAction(element: any, action: string) {
+    this.dialog.open(OdahuActionDialogComponent, {data: {type: action, item: element}, panelClass: 'modal-sm'})
+      .afterClosed().subscribe(result => {
+        result && this.odahuDeploymentService.odahuAction(element,  action).subscribe(v =>
+            this.buildGrid(),
+          error => this.toastr.error(`Odahu cluster ${action} failed!`, 'Oops!')
+        ) ;
+      }, error => this.toastr.error(error.message || `Odahu cluster ${action} failed!`, 'Oops!')
+    );
+  }
+
   public logAction(name) {
     this.auditService.sendDataToAudit({
       resource_name: name, info: `Open terminal, requested for notebook`, type: 'WEB_TERMINAL'
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.model.ts b/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.model.ts
index e1b1360..1465199 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.model.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.model.ts
@@ -56,6 +56,92 @@ export class ExploratoryModel {
 
   public static loadEnvironments(data: Array<any>) {
     if (data) {
+
+
+  //     return data.map((value) => {
+  //       const exploratory = value.exploratory.map(el => {
+  //         let provider;
+  //         if (el.cloud_provider) {
+  //           provider = el.cloud_provider.toLowerCase();
+  //         }
+  //         return new ExploratoryModel(
+  //           provider,
+  //           el.exploratory_name,
+  //           el.template_name,
+  //           el.image,
+  //           el.status,
+  //           el.shape,
+  //           el.computational_resources,
+  //           el.up_time,
+  //           el.exploratory_url,
+  //           value.shared[el.endpoint].edge_node_ip,
+  //           el.private_ip,
+  //           el.exploratory_user,
+  //           el.exploratory_pass,
+  //           value.shared[el.endpoint][DICTIONARY[provider].bucket_name],
+  //           value.shared[el.endpoint][DICTIONARY[provider].shared_bucket_name],
+  //           el.error_message,
+  //           el[DICTIONARY[provider].billing.cost],
+  //           el[DICTIONARY[provider].billing.currencyCode],
+  //           el.billing,
+  //           el.libs,
+  //           value.shared[el.endpoint][DICTIONARY[provider].user_storage_account_name],
+  //           value.shared[el.endpoint][DICTIONARY[provider].shared_storage_account_name],
+  //           value.shared[el.endpoint][DICTIONARY[provider].datalake_name],
+  //           value.shared[el.endpoint][DICTIONARY[provider].datalake_user_directory_name],
+  //           value.shared[el.endpoint][DICTIONARY[provider].datalake_shared_directory_name],
+  //           el.project,
+  //           el.endpoint,
+  //           el.tags
+  //         );
+  //       });
+  //
+  //       const odahu = value.odahu.map(el => {
+  //         let provider;
+  //         if (el.cloud_provider) {
+  //           provider = el.cloud_provider.toLowerCase();
+  //         } else {
+  //           provider = 'azure';
+  //         }
+  //         return new ExploratoryModel(
+  //           provider,
+  //           el.name,
+  //           el.template_name,
+  //           el.image,
+  //           el.status.toLowerCase(),
+  //           'odahu cluster',
+  //           [],
+  //           el.up_time,
+  //           el.urls,
+  //           value.shared[el.endpoint].edge_node_ip,
+  //           el.private_ip,
+  //           el.exploratory_user,
+  //           el.exploratory_pass,
+  //           el.grafana_admin,
+  //           el.grafana_pass,
+  //           el.error_message,
+  //           el[DICTIONARY[provider].billing.cost],
+  //           el[DICTIONARY[provider].billing.currencyCode],
+  //           [],
+  //           [],
+  //           '',
+  //           '',
+  //           '',
+  //           '',
+  //           '',
+  //           el.project,
+  //           el.endpoint,
+  //           el.tags
+  //         )});
+  //       return {
+  //         project: value.project,
+  //         exploratory: [...exploratory, ...odahu]
+  //       };
+  //     });
+  //   }
+  // }
+
+
       return data.map((value) => {
         return {
           project: value.project,
diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/odahu-action-dialog/index.ts b/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/odahu-action-dialog/index.ts
new file mode 100644
index 0000000..bfc5420
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/odahu-action-dialog/index.ts
@@ -0,0 +1,15 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { OdahuActionDialogComponent } from './odahu-action-dialog.component';
+import { MaterialModule } from '../../material.module';
+import {FormsModule} from '@angular/forms';
+
+export * from './odahu-action-dialog.component';
+
+@NgModule({
+  imports: [CommonModule, MaterialModule, FormsModule],
+  declarations: [OdahuActionDialogComponent],
+  entryComponents: [OdahuActionDialogComponent],
+  exports: [OdahuActionDialogComponent]
+})
+export class OdahuActionDialogModule {}
diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/odahu-action-dialog/odahu-action-dialog.component.ts b/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/odahu-action-dialog/odahu-action-dialog.component.ts
new file mode 100644
index 0000000..921ff99
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/odahu-action-dialog/odahu-action-dialog.component.ts
@@ -0,0 +1,50 @@
+import { Component, Inject } from '@angular/core';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+
+
+@Component({
+  selector: 'edge-action-dialog',
+  template: `
+  <div id="dialog-box">
+    <header class="dialog-header">
+      <h4 class="modal-title"><span class="action">{{data.type | titlecase}}</span> Odahu cluster</h4>
+      <button type="button" class="close" (click)="dialogRef.close()">&times;</button>
+    </header>
+      <div mat-dialog-content class="content message mat-dialog-content">
+          <h3>Odahu cluster <span class="strong">{{data.item.name}} </span>will be {{label[data.type]}}</h3>
+      <p class="m-top-20 action-text"><span class="strong">Do you want to proceed?</span></p>
+
+      <div class="text-center m-top-30 m-bott-30">
+        <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>
+      </div>
+  </div>
+  `,
+  styles: [`
+    .content { color: #718ba6; padding: 20px 50px; font-size: 14px; font-weight: 400; margin: 0; }
+    .info .confirm-dialog { color: #607D8B; }
+    header { display: flex; justify-content: space-between; color: #607D8B; }
+    h3 { font-weight: 300; }
+    header h4 i { vertical-align: bottom; }
+    header a i { font-size: 20px; }
+    header a:hover i { color: #35afd5; cursor: pointer; }
+    .action{text-transform: capitalize}
+    .action-text { text-align: center; }
+    label { font-size: 15px; font-weight: 500; font-family: "Open Sans",sans-serif; cursor: pointer; display: flex; align-items: center;}
+    label input {margin-top: 2px; margin-right: 5px;}
+  `]
+})
+
+export class OdahuActionDialogComponent {
+  public label = {
+    stop: 'stopped',
+    start: 'started',
+    terminate: 'terminated',
+  };
+
+  constructor(
+    public dialogRef: MatDialogRef<OdahuActionDialogComponent>,
+    @Inject(MAT_DIALOG_DATA) public data: any) {
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.component.html b/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.component.html
index 9e73f34..89de5e0 100644
--- a/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.component.html
@@ -88,6 +88,11 @@
               <span *ngIf="isExpanded; else projects">Projects</span>
               <ng-template #projects><i class="material-icons">dns</i></ng-template>
             </a>
+<!--            <a class="sub-nav-item" [style.margin-left.px]="isExpanded ? '30' : '0'" [routerLink]="['/odahu']"-->
+<!--               [routerLinkActive]="['active']" [routerLinkActiveOptions]="{exact:true}">-->
+<!--              <span *ngIf="isExpanded; else odahu">Odahu deployment</span>-->
+<!--              <ng-template #odahu><i class="material-icons">get_app</i></ng-template>-->
+<!--            </a>-->
             <a class="sub-nav-item" [style.margin-left.px]="isExpanded ? '30' : '0'"
               [routerLink]="['/environment_management']" [routerLinkActive]="['active']"
               [routerLinkActiveOptions]="{exact:true}">


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