You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datalab.apache.org by hs...@apache.org on 2022/06/29 12:27:56 UTC

[incubator-datalab] branch fix/DATALAB-2842/button-visibility-of-edge-node-recreation created (now f5e206073)

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

hshpak pushed a change to branch fix/DATALAB-2842/button-visibility-of-edge-node-recreation
in repository https://gitbox.apache.org/repos/asf/incubator-datalab.git


      at f5e206073 [DATALAB-2842] added logic by disable recreate button

This branch includes the following new commits:

     new f5e206073 [DATALAB-2842] added logic by disable recreate button

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



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


[incubator-datalab] 01/01: [DATALAB-2842] added logic by disable recreate button

Posted by hs...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

hshpak pushed a commit to branch fix/DATALAB-2842/button-visibility-of-edge-node-recreation
in repository https://gitbox.apache.org/repos/asf/incubator-datalab.git

commit f5e206073610ae80cff54dba8610061828e9e2b6
Author: Hennadii_Shpak <bo...@gmail.com>
AuthorDate: Wed Jun 22 13:46:14 2022 +0300

    [DATALAB-2842] added logic by disable recreate button
---
 .../create-odahu-cluster.component.ts              |   2 +-
 .../webapp/src/app/administration/project/index.ts |   8 +-
 .../administration/project/project-data.service.ts |   2 +-
 .../project/project-form/project-form.component.ts |   8 +-
 .../project-list/project-list.component.html       |  31 +++--
 .../project-list/project-list.component.scss       |  19 ++-
 .../project/project-list/project-list.component.ts | 141 ++++++++++++---------
 .../administration/project/project.component.ts    |  29 +----
 .../project/project.config.ts}                     |  16 +--
 .../project/project.model.ts}                      |  29 +++--
 .../webapp/src/app/core/directives/index.ts        |   8 +-
 .../directives/is-endpoint-active.directive.ts     |  60 +++++++++
 .../core/util/{index.ts => checkEndpointList.ts}   |  15 +--
 .../webapp/src/app/core/util/checkUtils.ts         |   2 +-
 .../resources/webapp/src/app/core/util/index.ts    |   1 +
 .../create-environment.component.ts                |  15 ++-
 .../resources-grid/resources-grid.component.ts     |   2 +-
 .../notification-dialog.component.ts               |   3 +-
 .../src/app/shared/navbar/navbar.component.html    |   4 +-
 19 files changed, 248 insertions(+), 147 deletions(-)

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
index a4f8360fa..e33e2e97e 100644
--- 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
@@ -22,11 +22,11 @@ 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';
+import { Project } from '../../project/project.model';
 
 
 @Component({
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/project/index.ts b/services/self-service/src/main/resources/webapp/src/app/administration/project/index.ts
index 8e57f0fb0..d1fd9d7ff 100644
--- a/services/self-service/src/main/resources/webapp/src/app/administration/project/index.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/project/index.ts
@@ -23,15 +23,16 @@ 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 { ProjectFormComponent } from './project-form/project-form.component';
 import { ProjectListComponent } from './project-list/project-list.component';
 
 import { ProjectComponent, EditProjectComponent } from './project.component';
 import { ProjectDataService } from './project-data.service';
-import {BubbleModule} from "../../shared/bubble";
 import {InformMessageModule} from '../../shared/inform-message';
+import { DirectivesModule } from '../../core/directives';
+import { BubbleModule } from '../../shared';
+import { UnderscorelessPipeModule } from '../../core/pipes';
 
 @NgModule({
     imports: [
@@ -42,7 +43,8 @@ import {InformMessageModule} from '../../shared/inform-message';
         FormControlsModule,
         UnderscorelessPipeModule,
         BubbleModule,
-        InformMessageModule
+        InformMessageModule,
+        DirectivesModule
     ],
   declarations: [ProjectComponent, EditProjectComponent, ProjectFormComponent, ProjectListComponent],
   entryComponents: [EditProjectComponent],
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/project/project-data.service.ts b/services/self-service/src/main/resources/webapp/src/app/administration/project/project-data.service.ts
index 0cb20d26b..ad9e5c380 100644
--- a/services/self-service/src/main/resources/webapp/src/app/administration/project/project-data.service.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/project/project-data.service.ts
@@ -22,7 +22,7 @@ import { BehaviorSubject, of } from 'rxjs';
 import { mergeMap} from 'rxjs/operators';
 
 import { ProjectService, EndpointService } from '../../core/services';
-import { Project } from './project.component';
+import { Project } from './project.model';
 
 @Injectable()
 export class ProjectDataService {
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/project/project-form/project-form.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/project/project-form/project-form.component.ts
index b5cd1b84d..d6fe75cde 100644
--- a/services/self-service/src/main/resources/webapp/src/app/administration/project/project-form/project-form.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/project/project-form/project-form.component.ts
@@ -27,9 +27,9 @@ import { Subscription } from 'rxjs';
 import { ProjectService, RolesGroupsService, EndpointService, UserAccessKeyService } from '../../../core/services';
 import { ProjectDataService } from '../project-data.service';
 import { CheckUtils, FileUtils, PATTERNS } from '../../../core/util';
-import { Project } from '../project.component';
 import { DICTIONARY } from '../../../../dictionary/global.dictionary';
 import {ConfirmationDialogComponent} from '../../../shared/modal-dialog/confirmation-dialog';
+import { Project } from '../project.model';
 
 export interface GenerateKey { privateKey: string; publicKey: string; }
 
@@ -88,7 +88,7 @@ export class ProjectFormComponent implements OnInit {
         () => {
           this.toastr.success('Project updated successfully!', 'Success!');
           this.update.emit();
-        }, 
+        },
         error => this.toastr.error(error.message || 'Project update failed!', 'Oops!')
       );
   }
@@ -96,7 +96,7 @@ export class ProjectFormComponent implements OnInit {
   public confirm(data) {
     if (this.item) {
       const deletedGroups = this.item.groups.filter((v) => !(this.projectForm.value.groups.includes(v)));
-      
+
       if (deletedGroups.length) {
         this.dialog.open(ConfirmationDialogComponent, {
           data: {notebook: deletedGroups, type: 5, manageAction: true}, panelClass: 'modal-md'
@@ -170,7 +170,7 @@ export class ProjectFormComponent implements OnInit {
 
 
   public selectOptions(list, key, select?) {
-    const filter = key === 'endpoints' ? list.filter(el => el.status === 'ACTIVE').map(el => el.name) : list
+    const filter = key === 'endpoints' ? list.filter(el => el.status === 'ACTIVE').map(el => el.name) : list;
     this.projectForm.controls[key].setValue(select ? filter : []);
   }
 
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/project/project-list/project-list.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/project/project-list/project-list.component.html
index bf4c8f739..ce81d953a 100644
--- a/services/self-service/src/main/resources/webapp/src/app/administration/project/project-list/project-list.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/project/project-list/project-list.component.html
@@ -28,7 +28,7 @@
       <td mat-cell *matCellDef="let element" class="groups">
         <div class="mat-chip-list-wrap scrolling">
           <mat-chip-list>
-            <mat-chip 
+            <mat-chip
               *ngFor="let group of element.groups"
               [matTooltip]="group"
               matTooltipPosition="above"
@@ -45,7 +45,7 @@
       <th mat-header-cell *matHeaderCellDef class="endpoints">
         <span class="label-endpoint"> Endpoint </span>
         <span class="label-endpoint-status"> Endpoint status </span>
-        <span class="label-status">Edge node status </span>
+        <span class="label-status">Edge node status</span>
       </th>
       <td mat-cell *matCellDef="let element" class="source endpoints">
         <div *ngIf="!element.endpoints?.length; else list">
@@ -63,9 +63,9 @@
                 {{ (endpoint.endpointStatus | titlecase) || 'N/A'}}
               </span>
             </div>
-            
+
             <span class="status resource-status" [ngClass]="endpoint?.status.toLowerCase() || ''">
-              {{ endpoint?.status.toLowerCase() }}
+              {{ endpoint?.status | titlecase }}
             </span>
           </div>
         </ng-template>
@@ -93,11 +93,24 @@
                 Stop edge node
               </a>
             </li>
-            <li class="project-seting-item " *ngIf="element.areTerminatedNode && isEndpointAvailable" (click)="openEdgeDialog('recreate', element)">
-              <i class="material-icons">refresh</i>
-              <a class="action">
-                Recreate edge node
-              </a>
+            <li
+                *ngIf="(element.areTerminatedNode || element.areStoppedNode) && isEndpointAvailable"
+                (click)="openEdgeDialog('recreate', element)"
+
+            >
+              <button class="project-setting-button"
+                      #recreateBtn
+                      datalabIsEndpointActive
+                      [endpointList]="element.endpoints"
+                      [matTooltip]="'Unable to recreate edge node if endpoint status is not active.'"
+                      matTooltipPosition="above"
+                      [matTooltipDisabled]="!isRecreateBtnDisabled(element.endpoints)"
+              >
+                <i class="material-icons">refresh</i>
+                <a class="action" >
+                  Recreate edge node
+                </a>
+              </button>
             </li>
             <li class="project-seting-item " *ngIf="element.areStoppedNode || element.areRunningNode" (click)="openEdgeDialog('terminate', element)">
               <i class="material-icons">phonelink_off</i>
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/project/project-list/project-list.component.scss b/services/self-service/src/main/resources/webapp/src/app/administration/project/project-list/project-list.component.scss
index 836b822f6..0abea7e84 100644
--- a/services/self-service/src/main/resources/webapp/src/app/administration/project/project-list/project-list.component.scss
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/project/project-list/project-list.component.scss
@@ -138,7 +138,8 @@ td.settings {
   }
 }
 
-.project-seting-item {
+.project-seting-item,
+.project-setting-button{
   display: flex;
   align-items: center;
   padding: 10px;
@@ -157,6 +158,18 @@ td.settings {
   }
 }
 
+.project-setting-button {
+  width: 100%;
+  background-color: transparent;
+  border: none;
+}
+
+.disabled-button {
+  color: rgba(0,0,0,0.26);
+  background-color: rgba(0,0,0,0.12);
+  cursor: not-allowed;
+}
+
 .material-icons {
   font-size: 18px;
   padding-top: 1px;
@@ -171,11 +184,11 @@ td.settings {
 }
 
 .actions-list {
-  padding: 10px 15px;
+  padding: 10px 0px;
 }
 
 .mat-chip {
   max-width: 200px !important;
   white-space: nowrap;
   display: inline-block;
-}
\ No newline at end of file
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/project/project-list/project-list.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/project/project-list/project-list.component.ts
index ca2c331a4..0086cb918 100644
--- a/services/self-service/src/main/resources/webapp/src/app/administration/project/project-list/project-list.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/project/project-list/project-list.component.ts
@@ -17,33 +17,37 @@
  * under the License.
  */
 
-import { Component, OnInit, Output, EventEmitter, OnDestroy, Inject, Input } from '@angular/core';
+import { Component, OnInit, Output, EventEmitter, OnDestroy, Inject, Input, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
 import { ToastrService } from 'ngx-toastr';
 import { MatTableDataSource } from '@angular/material/table';
 import { Subscription } from 'rxjs';
 import { MatDialogRef, MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog';
 
 import { ProjectDataService } from '../project-data.service';
-import { Project, Endpoint } from '../project.component';
 import {ProgressBarService} from '../../../core/services/progress-bar.service';
 import {EdgeActionDialogComponent} from '../../../shared/modal-dialog/edge-action-dialog';
 import { EndpointService } from '../../../core/services';
+import { Endpoint, ModifiedEndpoint, Project } from '../project.model';
+import { checkEndpointList } from '../../../core/util';
+import { EndpointStatus } from '../project.config';
 
 @Component({
   selector: 'project-list',
   templateUrl: './project-list.component.html',
   styleUrls: ['./project-list.component.scss', '../../../resources/computational/computational-resources-list/computational-resources-list.component.scss']
 })
-export class ProjectListComponent implements OnInit, OnDestroy {
+export class ProjectListComponent implements OnInit, OnDestroy, AfterViewInit {
+  @Input() isProjectAdmin: boolean;
+  @Output() editItem: EventEmitter<{}> = new EventEmitter();
+  @Output() toggleStatus: EventEmitter<{}> = new EventEmitter();
+
+  @ViewChild('recreateBtn') recreateBtn: ElementRef;
 
   displayedColumns: string[] = ['name', 'groups', 'endpoints', 'actions'];
   dataSource: Project[] | any = [];
   projectList: Project[];
   isEndpointAvailable: boolean;
 
-  @Input() isProjectAdmin: boolean;
-  @Output() editItem: EventEmitter<{}> = new EventEmitter();
-  @Output() toggleStatus: EventEmitter<{}> = new EventEmitter();
   private subscriptions: Subscription = new Subscription();
 
   constructor (
@@ -56,52 +60,23 @@ export class ProjectListComponent implements OnInit, OnDestroy {
     public dialog: MatDialog,
   ) { }
 
-  ngOnInit() {
+  ngOnInit(): void {
     this.getProjectList();
     this.getEndpointList();
   }
 
-  ngOnDestroy() {
-    this.subscriptions.unsubscribe();
-  }
+  ngAfterViewInit(): void {
 
-  private getProjectList() {
-    this.progressBarService.startProgressBar();
-    this.subscriptions.add(this.projectDataService._projects
-      .subscribe(
-        (value: Project[]) => {
-          this.projectList = value;
-          if (this.projectList) {
-            this.projectList.forEach(project => {
-              project.areRunningNode = this.areResoursesInStatuses(project.endpoints, ['RUNNING']);
-              project.areStoppedNode = this.areResoursesInStatuses(project.endpoints, ['STOPPED']);
-              project.areTerminatedNode = this.areResoursesInStatuses(project.endpoints, ['TERMINATED', 'FAILED']);
-            });
-          }
-          if (value) this.dataSource = new MatTableDataSource(value);
-          this.progressBarService.stopProgressBar();
-        }, 
-        () => this.progressBarService.stopProgressBar()
-      )
-    );
   }
 
-  private getEndpointList() {
-    this.endpointService.getEndpointsData().subscribe(
-      (response: Endpoint[] | []) => {
-        this.isEndpointAvailable = this.checkIsEndpointAvailable(response);
-      }
-    )
-  }
-
-  private checkIsEndpointAvailable(data: Endpoint[] | []): boolean {
-    return  data.length ? true : false;
+  ngOnDestroy(): void {
+    this.subscriptions.unsubscribe();
   }
 
   public showActiveInstances(): void {
     const filteredList = this.projectList.map(project => {
       project.endpoints = project.endpoints.filter((endpoint: Endpoint) => {
-        return endpoint.status !== 'TERMINATED' && endpoint.status !== 'TERMINATING' && endpoint.status !== 'FAILED'
+        return endpoint.status !== 'TERMINATED' && endpoint.status !== 'TERMINATING' && endpoint.status !== 'FAILED';
       });
       return project;
     });
@@ -113,37 +88,83 @@ export class ProjectListComponent implements OnInit, OnDestroy {
     this.editItem.emit(item);
   }
 
-  public openEdgeDialog(action, project) {
-    const endpoints = project.endpoints.filter(endpoint => {
+  public openEdgeDialog(action: string, project: Project) {
+    const endpoints = this.getFilteredEndpointList(action, project);
+
+    if (action === 'terminate' && endpoints.length === 1) {
+      this.toggleStatus.emit({ project, endpoint: endpoints, action, oneEdge: true });
+    } else {
+      this.dialog.open(EdgeActionDialogComponent, { data: { type: action, item: endpoints }, panelClass: 'modal-sm' })
+        .afterClosed().subscribe(
+        endpoint => {
+          if (endpoint && endpoint.length) {
+            this.toggleStatus.emit({project, endpoint, action});
+          }
+        },
+        error => this.toastr.error(error.message || `Endpoint ${action} failed!`, 'Oops!')
+      );
+    }
+  }
+
+  public areResoursesInStatuses(resources, statuses: Array<string>) {
+    return resources.some(resource => statuses.some(status => resource.status === status));
+  }
+
+  isRecreateBtnDisabled(endpointList: ModifiedEndpoint[]): boolean {
+    return checkEndpointList(endpointList);
+  }
+
+  private getFilteredEndpointList(action: string, project) {
+    return project.endpoints.filter(endpoint => {
       if (action === 'stop') {
-        return endpoint.status === 'RUNNING';
+        return endpoint.status === EndpointStatus.running;
       }
       if (action === 'start') {
-        return endpoint.status === 'STOPPED';
+        return endpoint.status === EndpointStatus.stopped;
       }
       if (action === 'terminate') {
-        return endpoint.status === 'RUNNING' || endpoint.status === 'STOPPED';
+        return endpoint.status === EndpointStatus.running || endpoint.status === EndpointStatus.stopped;
       }
       if (action === 'recreate') {
-        return endpoint.status === 'TERMINATED' || endpoint.status === 'FAILED';
+        return endpoint.status === EndpointStatus.terminated || endpoint.status === EndpointStatus.failed;
       }
     });
-    if (action === 'terminate' && endpoints.length === 1) {
-      this.toggleStatus.emit({ project, endpoint: endpoints, action, oneEdge: true });
-    } else {
-      this.dialog.open(EdgeActionDialogComponent, { data: { type: action, item: endpoints }, panelClass: 'modal-sm' })
-        .afterClosed().subscribe(
-          endpoint => {
-            if (endpoint && endpoint.length) {
-              this.toggleStatus.emit({project, endpoint, action});
-            }
-          }, 
-          error => this.toastr.error(error.message || `Endpoint ${action} failed!`, 'Oops!')
-        );
+  }
+
+  private getProjectList() {
+    this.progressBarService.startProgressBar();
+    this.subscriptions.add(this.projectDataService._projects
+      .subscribe(
+        (value: Project[]) => {
+          this.modifyProjectList(value);
+          if (value) this.dataSource = new MatTableDataSource(value);
+          this.progressBarService.stopProgressBar();
+        },
+        () => this.progressBarService.stopProgressBar()
+      )
+    );
+  }
+
+  private modifyProjectList(value: Project[]): void {
+    this.projectList = value;
+    if (this.projectList) {
+      this.projectList.forEach(project => {
+        project.areRunningNode = this.areResoursesInStatuses(project.endpoints, [EndpointStatus.running]);
+        project.areStoppedNode = this.areResoursesInStatuses(project.endpoints, [EndpointStatus.stopped]);
+        project.areTerminatedNode = this.areResoursesInStatuses(project.endpoints, [EndpointStatus.terminated, EndpointStatus.failed]);
+      });
     }
   }
 
-  public areResoursesInStatuses(resources, statuses: Array<string>) {
-    return resources.some(resource => statuses.some(status => resource.status === status));
+  private getEndpointList() {
+    this.endpointService.getEndpointsData().subscribe(
+      (response: Endpoint[] | []) => {
+        this.isEndpointAvailable = this.checkIsEndpointAvailable(response);
+      }
+    );
+  }
+
+  private checkIsEndpointAvailable(data: Endpoint[] | []): boolean {
+    return  data.length ? true : false;
   }
 }
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/project/project.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/project/project.component.ts
index 1409c7b80..61e486590 100644
--- a/services/self-service/src/main/resources/webapp/src/app/administration/project/project.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/project/project.component.ts
@@ -27,23 +27,8 @@ import {HealthStatusService, ProjectService } from '../../core/services';
 import { NotificationDialogComponent } from '../../shared/modal-dialog/notification-dialog';
 import { ProjectListComponent } from './project-list/project-list.component';
 import { EnvironmentsDataService } from '../management/management-data.service';
+import { Project } from './project.model';
 
-export interface Endpoint {
-  name: string;
-  status: string;
-  edgeInfo: any;
-}
-
-export interface Project {
-  name: string;
-  endpoints: any;
-  tag: string;
-  groups: string[];
-  shared_image_enabled?: boolean;
-  areStoppedNode?: boolean;
-  areTerminatedNode?: boolean;
-  areRunningNode?: boolean;
-}
 
 @Component({
   selector: 'datalab-project',
@@ -118,8 +103,8 @@ export class ProjectComponent implements OnInit, OnDestroy {
   }
 
   public toggleStatus($event) {
-    const data = { 
-      'project_name': $event.project.name, 
+    const data = {
+      'project_name': $event.project.name,
       endpoint: $event.endpoint.map(endpoint => endpoint.name),
       'edge_status': $event.endpoint.map(endpoint => endpoint.status)[0]
     };
@@ -129,11 +114,11 @@ export class ProjectComponent implements OnInit, OnDestroy {
   private toggleStatusRequest(data, action, isOnlyOneEdge?) {
     if ( action === 'terminate') {
       const projectsResources = this.resources
-        .filter(resource => resource.project === data.project_name && resource.resource_type !== "edge node");
+        .filter(resource => resource.project === data.project_name && resource.resource_type !== 'edge node');
 
       const activeProjectsResources = projectsResources?.length ? projectsResources
         .filter(expl => expl.status !== 'terminated' && expl.status !== 'terminating' && expl.status !== 'failed') : [];
-        
+
       const termResources = data.endpoint.reduce((res, endp) => {
         res.push(...activeProjectsResources.filter(resource => resource.endpoint === endp));
         return res;
@@ -143,7 +128,7 @@ export class ProjectComponent implements OnInit, OnDestroy {
         this.edgeNodeAction(data, action);
       } else {
         this.dialog.open(NotificationDialogComponent, { data: {
-            type: 'terminateNode', 
+            type: 'terminateNode',
             item: {action: data, resources: termResources}
           }, panelClass: 'modal-sm' })
           .afterClosed().subscribe(result => {
@@ -161,7 +146,7 @@ export class ProjectComponent implements OnInit, OnDestroy {
         () => {
           this.refreshGrid();
           this.toastr.success(`Edge node ${this.toEndpointAction(action)} is in progress!`, 'Processing!');
-        }, 
+        },
         error => {
           this.toastr.error(error.message, 'Oops!');
         }
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/util/index.ts b/services/self-service/src/main/resources/webapp/src/app/administration/project/project.config.ts
similarity index 74%
copy from services/self-service/src/main/resources/webapp/src/app/core/util/index.ts
copy to services/self-service/src/main/resources/webapp/src/app/administration/project/project.config.ts
index 8c04f80aa..fa333d082 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/util/index.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/project/project.config.ts
@@ -17,12 +17,10 @@
  * under the License.
  */
 
-export * from './http-status-codes';
-export * from './sortUtils';
-export * from './helpUtils';
-export * from './errorUtils';
-export * from './dateUtils';
-export * from './fileUtils';
-export * from './checkUtils';
-export * from './patterns';
-export * from './http-methods';
+export enum EndpointStatus {
+  terminated = 'TERMINATED',
+  terminating = 'TERMINATING',
+  failed = 'FAILED',
+  running = 'RUNNING',
+  stopped = 'STOPPED',
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/util/index.ts b/services/self-service/src/main/resources/webapp/src/app/administration/project/project.model.ts
similarity index 66%
copy from services/self-service/src/main/resources/webapp/src/app/core/util/index.ts
copy to services/self-service/src/main/resources/webapp/src/app/administration/project/project.model.ts
index 8c04f80aa..bfdc79030 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/util/index.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/project/project.model.ts
@@ -17,12 +17,23 @@
  * under the License.
  */
 
-export * from './http-status-codes';
-export * from './sortUtils';
-export * from './helpUtils';
-export * from './errorUtils';
-export * from './dateUtils';
-export * from './fileUtils';
-export * from './checkUtils';
-export * from './patterns';
-export * from './http-methods';
+export interface Endpoint {
+  name: string;
+  status: string;
+  edgeInfo: any;
+}
+
+export interface ModifiedEndpoint extends Endpoint {
+  endpointStatus?: 'ACTIVE' | 'INACTIVE';
+}
+
+export interface Project {
+  name: string;
+  endpoints: any;
+  tag: string;
+  groups: string[];
+  shared_image_enabled?: boolean;
+  areStoppedNode?: boolean;
+  areTerminatedNode?: boolean;
+  areRunningNode?: boolean;
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/directives/index.ts b/services/self-service/src/main/resources/webapp/src/app/core/directives/index.ts
index 83a0eaee4..9f8dcb2b2 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/directives/index.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/directives/index.ts
@@ -22,14 +22,12 @@ import { NgModule } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { ClickOutsideDirective } from './click-outside.directive';
 import { ScrollDirective } from './scrollTo.directive';
-
-export * from './scrollTo.directive';
-export * from './click-outside.directive';
+import { IsEndpointsActiveDirective } from './is-endpoint-active.directive';
 
 @NgModule({
   imports: [CommonModule],
-  declarations: [ClickOutsideDirective, ScrollDirective],
-  exports: [ClickOutsideDirective, ScrollDirective]
+  declarations: [ClickOutsideDirective, ScrollDirective, IsEndpointsActiveDirective],
+  exports: [ClickOutsideDirective, ScrollDirective, IsEndpointsActiveDirective]
 })
 
 export class DirectivesModule { }
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/directives/is-endpoint-active.directive.ts b/services/self-service/src/main/resources/webapp/src/app/core/directives/is-endpoint-active.directive.ts
new file mode 100644
index 000000000..0f9db68bd
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/core/directives/is-endpoint-active.directive.ts
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Directive, ElementRef, HostListener, Input, OnInit, Renderer2 } from '@angular/core';
+
+import { ModifiedEndpoint } from '../../administration/project/project.model';
+import { checkEndpointList } from '../util';
+
+
+@Directive({
+  selector: '[datalabIsEndpointActive]'
+})
+export class IsEndpointsActiveDirective implements OnInit {
+  @Input() endpointList: ModifiedEndpoint[];
+
+  private isButtonDisabled: boolean = false;
+
+  constructor (
+    private el: ElementRef,
+    private renderer: Renderer2
+  ) { }
+
+  ngOnInit(): void {
+    this.checkEndpointList(this.endpointList);
+    this.setStyle();
+  }
+
+  @HostListener('click', ['$event'])
+  onClick(event: Event): void {
+    if (this.isButtonDisabled) {
+      event.stopPropagation();
+    }
+  }
+
+  private setStyle() {
+    if (this.isButtonDisabled) {
+      this.renderer.addClass(this.el.nativeElement, 'disabled-button');
+    }
+  }
+
+  private checkEndpointList(endpointList: ModifiedEndpoint[]): void {
+    this.isButtonDisabled = checkEndpointList(endpointList);
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/util/index.ts b/services/self-service/src/main/resources/webapp/src/app/core/util/checkEndpointList.ts
similarity index 72%
copy from services/self-service/src/main/resources/webapp/src/app/core/util/index.ts
copy to services/self-service/src/main/resources/webapp/src/app/core/util/checkEndpointList.ts
index 8c04f80aa..99f927ddc 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/util/index.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/util/checkEndpointList.ts
@@ -17,12 +17,9 @@
  * under the License.
  */
 
-export * from './http-status-codes';
-export * from './sortUtils';
-export * from './helpUtils';
-export * from './errorUtils';
-export * from './dateUtils';
-export * from './fileUtils';
-export * from './checkUtils';
-export * from './patterns';
-export * from './http-methods';
+import { ModifiedEndpoint } from '../../administration/project/project.model';
+
+export const checkEndpointList = (endpointList: ModifiedEndpoint[]): boolean => {
+  return endpointList.every(({status, endpointStatus}) => (status === 'TERMINATED' || status === 'FAILED')
+    && endpointStatus === 'INACTIVE');
+};
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/util/checkUtils.ts b/services/self-service/src/main/resources/webapp/src/app/core/util/checkUtils.ts
index d1e6f535e..ca3e5a2c2 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/util/checkUtils.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/util/checkUtils.ts
@@ -28,7 +28,7 @@ export class CheckUtils {
     STOPPING: 'DISCONNECTING',
     STOPPED: 'DISCONNECTED'
   };
-  
+
   public static isJSON(str) {
     try {
       JSON.parse(str);
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/util/index.ts b/services/self-service/src/main/resources/webapp/src/app/core/util/index.ts
index 8c04f80aa..b00239022 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/util/index.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/util/index.ts
@@ -26,3 +26,4 @@ export * from './fileUtils';
 export * from './checkUtils';
 export * from './patterns';
 export * from './http-methods';
+export * from './checkEndpointList';
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/create-environment/create-environment.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/create-environment/create-environment.component.ts
index d0b107ea5..debc6515c 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/create-environment/create-environment.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/create-environment/create-environment.component.ts
@@ -22,7 +22,6 @@ import { FormGroup, FormBuilder, Validators } from '@angular/forms';
 import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
 import { ToastrService } from 'ngx-toastr';
 
-import { Project } from '../../../administration/project/project.component';
 import { UserResourceService, ProjectService } from '../../../core/services';
 import { CheckUtils, SortUtils, HTTP_STATUS_CODES, PATTERNS, HelpUtils } from '../../../core/util';
 import { DICTIONARY } from '../../../../dictionary/global.dictionary';
@@ -30,6 +29,7 @@ import { CLUSTER_CONFIGURATION } from '../../computational/computational-resourc
 import { tap } from 'rxjs/operators';
 import { timer } from 'rxjs';
 import { TemplateName } from '../../../core/models';
+import { Project } from '../../../administration/project/project.model';
 
 @Component({
   selector: 'create-environment',
@@ -181,7 +181,8 @@ export class ExploratoryEnvironmentCreateComponent implements OnInit {
 
       this.gpuTypes = template?.computationGPU ? HelpUtils.sortGpuTypes(template.computationGPU) : [];
 
-      if(template?.image === 'docker.datalab-tensor' /**|| template?.image === 'docker.datalab-jupyter-conda'|| template?.image === 'docker.datalab-jupyter-gpu' */|| template?.image === 'docker.datalab-deeplearning') {
+      // tslint:disable-next-line:max-line-length
+      if (template?.image === 'docker.datalab-tensor' /**|| template?.image === 'docker.datalab-jupyter-conda'|| template?.image === 'docker.datalab-jupyter-gpu' */|| template?.image === 'docker.datalab-deeplearning') {
         this.addGpuFields();
       }
     }
@@ -272,9 +273,9 @@ export class ExploratoryEnvironmentCreateComponent implements OnInit {
   }
 
   public setInstanceSize() {
-    const {image, computationGPU} = this.currentTemplate
-    if(image === this.templateName.jupyterJpu) {
-      this.createExploratoryForm.get('gpu_count').setValue(computationGPU[0])
+    const {image, computationGPU} = this.currentTemplate;
+    if (image === this.templateName.jupyterJpu) {
+      this.createExploratoryForm.get('gpu_count').setValue(computationGPU[0]);
     } else {
         const controls = ['gpu_type', 'gpu_count'];
         controls.forEach(control => {
@@ -329,9 +330,9 @@ export class ExploratoryEnvironmentCreateComponent implements OnInit {
         (res: any) => {
           this.images = res.filter(el => el.status === 'CREATED');
 
-          if(this.selectedCloud === 'gcp' && this.currentTemplate.image === 'docker.datalab-deeplearning') {
+          if (this.selectedCloud === 'gcp' && this.currentTemplate.image === 'docker.datalab-deeplearning') {
             this.currentTemplate.exploratory_environment_images = this.currentTemplate.exploratory_environment_images.map(image => {
-              return {name: image['Image family'] ?? image.name, description: image['Description'] ?? image.description}
+              return {name: image['Image family'] ?? image.name, description: image['Description'] ?? image.description};
             });
             this.images.push(...this.currentTemplate.exploratory_environment_images);
           }
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 28b79ecd8..4677f57d2 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
@@ -17,7 +17,6 @@
  * under the License.
  */
 
-import { Project } from '../../administration/project/project.component';
 import {
   Component,
   EventEmitter,
@@ -47,6 +46,7 @@ import { NotebookModel } from '../exploratory/notebook.model';
 import { AuditService } from '../../core/services/audit.service';
 import { CompareUtils } from '../../core/util/compareUtils';
 import { OdahuActionDialogComponent } from '../../shared/modal-dialog/odahu-action-dialog';
+import { Project } from '../../administration/project/project.model';
 
 export interface SharedEndpoint {
   edge_node_ip: string;
diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/notification-dialog/notification-dialog.component.ts b/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/notification-dialog/notification-dialog.component.ts
index 2cdee43fc..4f532c0e8 100644
--- a/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/notification-dialog/notification-dialog.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/notification-dialog/notification-dialog.component.ts
@@ -19,7 +19,8 @@
 
 import { Component, Inject } from '@angular/core';
 import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
-import {Endpoint} from '../../../administration/project/project.component';
+
+import { Endpoint } from '../../../administration/project/project.model';
 
 @Component({
   selector: 'notification-dialog',
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 4ec0819a6..8ae321ffe 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
@@ -28,8 +28,8 @@
       <span class="line"></span>
     </button>
 
-    <a [routerLink]="['/resources_list']" class="navbar-logo">
-      <img src="assets/svg/logo.svg" alt="">
+    <a [routerLink]="['/instances']" class="navbar-logo">
+      <img src="assets/svg/logo.svg" alt="datalab">
     </a>
   </div>
 


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