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 13:57:29 UTC

[incubator-datalab] 02/05: Merge ODAHU UI to develop

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

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

commit e1c199268779dfd2af18b949dc5b98361f50dc7a
Author: Dmytro_Gnatyshyn <di...@ukr.net>
AuthorDate: Wed Dec 2 15:56:40 2020 +0200

    Merge ODAHU UI to develop
---
 .../create-legion-cluster.component.html           |  93 +++++++++++++++++++
 .../create-legion-cluster.component.scss           |   7 ++
 .../create-legion-cluster.component.ts             | 103 +++++++++++++++++++++
 .../create-legion-claster/index.ts                 |  45 +++++++++
 .../app/administration/legion-deployment/index.ts  |  50 ++++++++++
 .../legion-deployment-data.service.ts              |  27 ++++++
 .../legion-deployment.component.html               |  42 +++++++++
 .../legion-deployment.component.scss}              |   0
 .../legion-deployment.component.ts                 |  59 ++++++++++++
 .../legion-list/legion-list.component.html         |  94 +++++++++++++++++++
 .../legion-list/legion-list.component.scss         |  77 +++++++++++++++
 .../legion-list/legion-list.component.ts           |  48 ++++++++++
 .../management-grid/management-grid.component.html |   2 +-
 .../app/core/services/legion-deployment.service.ts |  35 +++++++
 .../resources-grid/resources-grid.component.html   |  30 +++++-
 .../resources-grid/resources-grid.component.scss   |  13 +++
 .../resources-grid/resources-grid.component.ts     |  20 +++-
 .../resources-grid/resources-grid.model.ts         |  86 +++++++++++++++++
 .../modal-dialog/odahu-action-dialog/index.ts      |  15 +++
 .../odahu-action-dialog.component.ts               |  50 ++++++++++
 20 files changed, 891 insertions(+), 5 deletions(-)

diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/create-legion-claster/create-legion-cluster.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/create-legion-claster/create-legion-cluster.component.html
new file mode 100644
index 0000000..ff04e55
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/create-legion-claster/create-legion-cluster.component.html
@@ -0,0 +1,93 @@
+<div class="create-legion-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]="createLegionClusterForm" *ngIf="createLegionClusterForm" 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="!createLegionClusterForm.controls.name.valid && createLegionClusterForm.controls.name.dirty && !createLegionClusterForm.controls.name.hasError('duplication')">
+              Odahu cluster name can only contain letters and numbers
+            </span>
+            <span class="error" *ngIf="createLegionClusterForm.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="!createLegionClusterForm.controls.custom_tag.valid && createLegionClusterForm.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]="!createLegionClusterForm.valid" (click)="createOdahuCluster(createLegionClusterForm.value)">
+            Create
+          </button>
+        </div>
+
+      </form>
+    </div>
+  </div>
+</div>
+
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/create-legion-claster/create-legion-cluster.component.scss b/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/create-legion-claster/create-legion-cluster.component.scss
new file mode 100644
index 0000000..324f0df
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/create-legion-claster/create-legion-cluster.component.scss
@@ -0,0 +1,7 @@
+.create-legion-cluster{
+  .error{
+    position: absolute;
+    left: 0;
+    top: 36px;
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/create-legion-claster/create-legion-cluster.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/create-legion-claster/create-legion-cluster.component.ts
new file mode 100644
index 0000000..7aa941d
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/create-legion-claster/create-legion-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, LegionDeploymentService } from '../../../core/services';
+
+import { DICTIONARY } from '../../../../dictionary/global.dictionary';
+import {CheckUtils, PATTERNS} from '../../../core/util';
+
+
+@Component({
+  selector: 'create-legion-cluster',
+  templateUrl: 'create-legion-cluster.component.html',
+  styleUrls: ['./create-legion-cluster.component.scss']
+})
+
+export class CreateLegionClusterComponent implements OnInit {
+  readonly DICTIONARY = DICTIONARY;
+  public createLegionClusterForm: FormGroup;
+
+  projects: Project[] = [];
+  endpoints: Array<String> = [];
+
+  constructor(
+    @Inject(MAT_DIALOG_DATA) public data: any,
+    public toastr: ToastrService,
+    public dialogRef: MatDialogRef<CreateLegionClusterComponent>,
+    private _fb: FormBuilder,
+    private projectService: ProjectService,
+    private legionDeploymentService: LegionDeploymentService,
+  ) {
+  }
+
+  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.createLegionClusterForm = 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.legionDeploymentService.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/legion-deployment/create-legion-claster/index.ts b/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/create-legion-claster/index.ts
new file mode 100644
index 0000000..a3adc9c
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/create-legion-claster/index.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 { KeysPipeModule, UnderscorelessPipeModule } from '../../../core/pipes';
+import {CreateLegionClusterComponent} from "./create-legion-cluster.component";
+
+export * from './create-legion-cluster.component';
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    ReactiveFormsModule,
+    FormControlsModule,
+    MaterialModule,
+    KeysPipeModule,
+    UnderscorelessPipeModule
+  ],
+  declarations: [CreateLegionClusterComponent],
+  entryComponents: [CreateLegionClusterComponent],
+  exports: [CreateLegionClusterComponent]
+})
+export class CreateLegionClusterModule { }
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/index.ts b/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/index.ts
new file mode 100644
index 0000000..fa28e36
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/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 {LegionDeploymentComponent} from "./legion-deployment.component";
+import {LegionDeploymentDataService} from "./legion-deployment-data.service";
+import {LegionListComponent} from "./legion-list/legion-list.component";
+import {CreateLegionClusterModule} from "./create-legion-claster";
+
+@NgModule({
+  imports: [
+    CommonModule,
+    FormsModule,
+    ReactiveFormsModule,
+    MaterialModule,
+    FormControlsModule,
+    UnderscorelessPipeModule,
+    BubbleModule,
+    CreateLegionClusterModule
+  ],
+  declarations: [LegionDeploymentComponent, LegionListComponent],
+  entryComponents: [],
+  providers: [LegionDeploymentDataService],
+  exports: [LegionDeploymentComponent]
+})
+export class LegionDeploymentModule { }
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/legion-deployment-data.service.ts b/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/legion-deployment-data.service.ts
new file mode 100644
index 0000000..83dfeaf
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/legion-deployment-data.service.ts
@@ -0,0 +1,27 @@
+import { Injectable } from '@angular/core';
+import {BehaviorSubject, Observable} from 'rxjs';
+import {LegionDeploymentService} from '../../core/services';
+
+
+@Injectable({
+  providedIn: 'root'
+})
+
+export class LegionDeploymentDataService {
+  _legionClasters = new BehaviorSubject<any>(null);
+
+  constructor(private legionDeploymentService: LegionDeploymentService) {
+    this.getClastersList();
+  }
+
+  public updateClasters(): void {
+    this.getClastersList();
+  }
+
+  private getClastersList(): void {
+   this.legionDeploymentService.getOduhuClustersList().subscribe(
+      (response: any ) => {
+        return this._legionClasters.next(response);
+      });
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/legion-deployment.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/legion-deployment.component.html
new file mode 100644
index 0000000..1368180
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/legion-deployment.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)="createLegionCluster()">
+        <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>
+    <legion-list>
+    </legion-list>
+  </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/legion-deployment/legion-deployment.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/legion-deployment/legion-deployment.component.scss
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/legion-deployment.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/legion-deployment.component.ts
new file mode 100644
index 0000000..263debf
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/legion-deployment.component.ts
@@ -0,0 +1,59 @@
+import { Component, OnInit } from '@angular/core';
+import {LegionDeploymentDataService} from './legion-deployment-data.service';
+import {Subscription} from 'rxjs';
+import {MatDialog} from '@angular/material/dialog';
+import {ToastrService} from 'ngx-toastr';
+import {CreateLegionClusterComponent} from './create-legion-claster/create-legion-cluster.component';
+import {HealthStatusService, LegionDeploymentService} from '../../core/services';
+
+export interface OdahuCluster {
+  name: string;
+  project: string;
+  endpoint: string;
+}
+
+@Component({
+  selector: 'dlab-legion-deployment',
+  templateUrl: './legion-deployment.component.html',
+  styleUrls: ['./legion-deployment.component.scss']
+})
+export class LegionDeploymentComponent implements OnInit {
+
+  private legionClastersList: any[];
+  private subscriptions: Subscription = new Subscription();
+  private healthStatus;
+
+  constructor(
+    private legionDeploymentDataService: LegionDeploymentDataService,
+    private dialog: MatDialog,
+    public toastr: ToastrService,
+    public legionDeploymentService: LegionDeploymentService,
+    private healthStatusService: HealthStatusService,
+  ) { }
+
+  ngOnInit() {
+    this.getEnvironmentHealthStatus();
+    this.subscriptions.add(this.legionDeploymentDataService._legionClasters.subscribe(
+      (value) => {
+        if (value) this.legionClastersList = value;
+      }));
+    this.refreshGrid();
+  }
+
+  public createLegionCluster(): void {
+    this.dialog.open(CreateLegionClusterComponent, {  data: this.legionClastersList, 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.legionDeploymentDataService.updateClasters();
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/legion-list/legion-list.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/legion-list/legion-list.component.html
new file mode 100644
index 0000000..a670b2f
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/legion-list/legion-list.component.html
@@ -0,0 +1,94 @@
+
+<table mat-table [dataSource]="dataSource" class="legion-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="legion-name">
+    <th mat-header-cell *matHeaderCellDef class="legion-name"> Odahu cluster name </th>
+    <td mat-cell *matCellDef="let element" class="legion-name">
+      <span *ngIf="element && element.name">{{element.name}}</span>
+    </td>
+    <td mat-footer-cell *matFooterCellDef></td>
+  </ng-container>
+
+  <ng-container matColumnDef="legion-status">
+    <th mat-header-cell *matHeaderCellDef class="legion-status"> Odahu cluster status </th>
+    <td mat-cell *matCellDef="let element" class="legion-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="legion-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]="legionClustersList && legionClustersList.length" mat-footer-row *matFooterRowDef="['placeholder']"></tr>
+</table>
+
+
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/legion-list/legion-list.component.scss b/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/legion-list/legion-list.component.scss
new file mode 100644
index 0000000..21fc43e
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/legion-list/legion-list.component.scss
@@ -0,0 +1,77 @@
+.legion-clasters-table {
+  width: 100%;
+
+  .project, .endpoint, .legion-name, .legion-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/legion-deployment/legion-list/legion-list.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/legion-list/legion-list.component.ts
new file mode 100644
index 0000000..7e7913e
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/legion-deployment/legion-list/legion-list.component.ts
@@ -0,0 +1,48 @@
+import { Component, OnInit } from '@angular/core';
+import {Subscription} from 'rxjs';
+import {LegionDeploymentDataService} from '../legion-deployment-data.service';
+import { MatTableDataSource } from '@angular/material/table';
+import {LegionDeploymentService} 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: 'legion-list',
+  templateUrl: './legion-list.component.html',
+  styleUrls: ['./legion-list.component.scss']
+})
+export class LegionListComponent implements OnInit {
+  private legionClustersList: any[];
+  private subscriptions: Subscription = new Subscription();
+  public dataSource: MatTableDataSource<any>;
+  displayedColumns: string[] = [ 'legion-name', 'project', 'endpoint-url', 'legion-status', 'actions'];
+
+  constructor(
+    private legionDeploymentDataService: LegionDeploymentDataService,
+    private legionDeploymentService: LegionDeploymentService,
+    public toastr: ToastrService,
+    public dialog: MatDialog
+  ) { }
+
+  ngOnInit() {
+    this.subscriptions.add(this.legionDeploymentDataService._legionClasters.subscribe(
+      (value) => {
+        if (value) {
+          this.legionClustersList = 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.legionDeploymentService.odahuAction(element,  action).subscribe(v =>
+          this.legionDeploymentDataService.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/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/core/services/legion-deployment.service.ts b/services/self-service/src/main/resources/webapp/src/app/core/services/legion-deployment.service.ts
new file mode 100644
index 0000000..1b0872e
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/core/services/legion-deployment.service.ts
@@ -0,0 +1,35 @@
+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()
+
+export class LegionDeploymentService {
+  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/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..6af8f68 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)="printDetailLegionModal(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..3e1bb78 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;
+}
+
+.legion-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..dcfa052 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, LegionDeploymentService} 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 legionDeploymentService: LegionDeploymentService,
   ) { }
 
   ngOnInit(): void {
@@ -227,6 +229,11 @@ export class ResourcesGridComponent implements OnInit {
       .afterClosed().subscribe(() => this.buildGrid());
   }
 
+  public printDetailLegionModal(data): void {
+    this.dialog.open(DetailDialogComponent, { data: {legion: 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());
@@ -445,6 +452,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.legionDeploymentService.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) {
+  }
+}


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