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/10/19 16:32:24 UTC

[incubator-datalab] 01/01: added connected platform component

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

hshpak pushed a commit to branch epm-connected-platform
in repository https://gitbox.apache.org/repos/asf/incubator-datalab.git

commit 7a9c52b578145a58804b02af72cf7fdfcdff5928
Author: Hennadii_Shpak <bo...@gmail.com>
AuthorDate: Wed Oct 19 19:32:00 2022 +0300

    added connected platform component
---
 .../administration/management/management.model.ts  |  12 ++-
 .../resources/webapp/src/app/app.routing.module.ts |  63 +++++------
 .../configs/connected-platforms.model.ts}          |  17 +--
 .../configs/routing-list.config.ts}                |  29 +++---
 .../pipes/normalize-link/index.ts}                 |  12 +--
 .../pipes/normalize-link/normalize-link.pipe.ts}   |  14 +--
 .../src/app/core/services/appRouting.service.ts    |   3 +-
 .../services/applicationServiceFacade.service.ts   |  30 +++++-
 .../services/connected-platform-api.service.ts     |  54 ++++++++++
 .../connected-platform-dialog.component.html       |  80 ++++++++++++++
 .../connected-platform-dialog.component.scss       |  98 +++++++++++++++++
 .../connected-platform-dialog.component.ts         |  70 +++++++++++++
 .../connected-platforms-routing.module.ts}         |  21 ++--
 .../connected-platforms.component.html             |  96 +++++++++++++++++
 .../connected-platforms.component.scss             |  99 ++++++++++++++++++
 .../connected-platforms.component.ts               | 114 ++++++++++++++++++++
 .../connected-platforms.config.ts}                 |  30 +++---
 .../connected-platforms.models.ts}                 |  24 +++--
 .../connected-platforms.module.ts                  |  53 ++++++++++
 .../connected-platforms.service.ts                 |  61 +++++++++++
 .../warning-dialog/warning-dialog.component.html}  |  21 ++--
 .../warning-dialog/warning-dialog.component.scss}  |  26 +++--
 .../warning-dialog/warning-dialog.component.ts     |  49 +++++++++
 .../access-denied/access-denied.component.ts       |  10 +-
 .../not-found/not-found.component.html             |   4 +-
 .../service-pages/not-found/not-found.component.ts |   5 +-
 .../src/app/service-pages/service-pages.module.ts  |   5 +-
 .../modal-btn/modal-btn.component.html}            |  27 ++---
 .../modal-btn/modal-btn.component.scss}            |  16 +--
 .../modal-parts/modal-btn/modal-btn.component.ts}  |  20 +++-
 .../modal-header/modal-header.component.html}      |  20 ++--
 .../modal-header/modal-header.component.scss}      |  35 ++++---
 .../modal-header/modal-header.component.ts}        |  21 ++--
 .../modal-parts/modal-parts.module.ts}             |  24 +++--
 .../src/app/shared/navbar/navbar.component.html    | 116 ++++++++++++---------
 .../src/app/shared/navbar/navbar.component.ts      |  31 +++---
 .../webapp/src/app/shared/navbar/navbar.config.ts  |  22 ++--
 37 files changed, 1165 insertions(+), 267 deletions(-)

diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/management.model.ts b/services/self-service/src/main/resources/webapp/src/app/administration/management/management.model.ts
index 56ce00ff7..14b5b15f2 100644
--- a/services/self-service/src/main/resources/webapp/src/app/administration/management/management.model.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/management.model.ts
@@ -85,8 +85,14 @@ export interface GeneralEnvironmentStatus {
   status: string;
   projectAssigned: boolean;
   bucketBrowser: object;
+  connectedPlatforms: ConnectedPlatformsStatus;
 }
 
+export interface ConnectedPlatformsStatus {
+  add: boolean;
+  disconnect: boolean;
+  view: boolean;
+}
 
 export class ManagementConfigModel {
 
@@ -117,8 +123,8 @@ export class ManagementConfigModel {
 
 export interface ModalData {
   action: ActionsType;
-  resource_name?: any; 
-  user?: any, 
+  resource_name?: any;
+  user?: any,
   type: string;
   notebooks?: any
 }
@@ -134,4 +140,4 @@ export enum ActionsType {
   start = 'start',
   run = 'run',
   recreate = 'recreate'
-}
\ No newline at end of file
+}
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 d166b7d40..3ff739602 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
@@ -37,12 +37,13 @@ import {ProjectAdminGuard} from './core/services/projectAdmin.guard';
 import {ReportingComponent} from './reports/reporting/reporting.component';
 import {OdahuComponent} from './administration/odahu/odahu.component';
 import {AuditComponent} from './reports/audit/audit.component';
+import { RoutingListConfig } from './core/configs/routing-list.config';
 
 const routes: Routes = [
   {
-    path: 'login',
+    path: RoutingListConfig.login,
     component: LoginComponent
-  }, 
+  },
   {
     path: '',
     canActivate: [CheckParamsGuard],
@@ -50,77 +51,81 @@ const routes: Routes = [
     children: [
       {
         path: '',
-        redirectTo: 'resources_list',
+        redirectTo: RoutingListConfig.instances,
         pathMatch: 'full'
-      }, 
+      },
       {
-        path: 'resources_list',
+        path: RoutingListConfig.instances,
         component: ResourcesComponent,
         canActivate: [AuthorizationGuard]
-      }, 
+      },
       {
-        path: 'billing_report',
+        path: RoutingListConfig.connectedPlatforms,
+        loadChildren: () => import('./resources/connected-platforms/connected-platforms.module').then(m => m.ConnectedPlatformsModule)
+      },
+      {
+        path: RoutingListConfig.billing,
         component: ReportingComponent,
         canActivate: [AuthorizationGuard, CloudProviderGuard]
-      }, 
+      },
       {
-        path: 'projects',
+        path: RoutingListConfig.projects,
         component: ProjectComponent,
         canActivate: [AuthorizationGuard, AdminGuard],
       },
       {
-      //   path: 'odahu',
-      //   component: OdahuComponent,
-      //   canActivate: [AuthorizationGuard, AdminGuard],
-      // }, {
-        path: 'roles',
+        //   path: 'odahu',
+        //   component: OdahuComponent,
+        //   canActivate: [AuthorizationGuard, AdminGuard],
+        // }, {
+        path: RoutingListConfig.users,
         component: RolesComponent,
         canActivate: [AuthorizationGuard, AdminGuard],
-      }, 
+      },
       {
-        path: 'environment_management',
+        path: RoutingListConfig.resources,
         component: ManagementComponent,
         canActivate: [AuthorizationGuard, AdminGuard]
-      }, 
+      },
       {
-        path: 'configuration',
+        path: RoutingListConfig.configuration,
         component: ConfigurationComponent,
         canActivate: [AuthorizationGuard, AdminGuard, ProjectAdminGuard]
       },
       {
-        path: 'swagger',
+        path: RoutingListConfig.swagger,
         component: SwaggerComponent,
         canActivate: [AuthorizationGuard]
-      }, 
+      },
       {
-        path: 'help/publickeyguide',
+        path: RoutingListConfig.publickeyguide,
         component: PublicKeyGuideComponent,
         canActivate: [AuthorizationGuard]
-      }, 
+      },
       {
-        path: 'help/accessnotebookguide',
+        path: RoutingListConfig.accessnotebookguide,
         component: AccessNotebookGuideComponent,
         canActivate: [AuthorizationGuard]
       },
       {
-        path: 'audit',
+        path: RoutingListConfig.audit,
         component: AuditComponent,
         canActivate: [AuthorizationGuard, AuditGuard],
       },
     ]
-  }, 
+  },
   {
     path: 'terminal/:id/:endpoint',
     component: WebterminalComponent
-  }, 
+  },
   {
     path: '403',
     component: AccessDeniedComponent,
     canActivate: [AuthorizationGuard]
-  }, 
+  },
   {
-  path: '**',
-  component: NotFoundComponent
+    path: '**',
+    component: NotFoundComponent
   }
 ];
 
diff --git a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts b/services/self-service/src/main/resources/webapp/src/app/core/configs/connected-platforms.model.ts
similarity index 77%
copy from services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts
copy to services/self-service/src/main/resources/webapp/src/app/core/configs/connected-platforms.model.ts
index 34f860f16..2344ed14a 100644
--- a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/configs/connected-platforms.model.ts
@@ -17,11 +17,14 @@
  * under the License.
  */
 
-import { Component } from '@angular/core';
+export enum ModalTitle {
+  share = 'Share image',
+  terminate = 'Terminate image',
+  unShare = '! Warning',
+  addPlatform = 'Add platform'
+}
 
-@Component({
-    selector: 'not-found',
-    templateUrl: 'not-found.component.html',
-    styleUrls: ['not-found.component.scss']
-})
-export class NotFoundComponent { }
+export enum URL_Chunk {
+  sharingInfo = 'sharing_info',
+  autocomplete = 'share_autocomplete'
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.config.ts b/services/self-service/src/main/resources/webapp/src/app/core/configs/routing-list.config.ts
similarity index 66%
copy from services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.config.ts
copy to services/self-service/src/main/resources/webapp/src/app/core/configs/routing-list.config.ts
index 38df6d2c5..171f80c4e 100644
--- a/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.config.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/configs/routing-list.config.ts
@@ -17,19 +17,18 @@
  * under the License.
  */
 
-export const sideBarNamesConfig: Record<string, string> = {
-    resourses: 'Resources',
-    reports: 'Reports',
-    audit: 'Audit',
-    billing: 'Billing',
-    administration: 'Administration',
-    users: 'Users',
-    projects: 'Projects',
-    resources: 'Resources',
-    configuration: 'Configuration'
-}
-
-export interface UserInfo {
-    email: string;
-    name: string;
+export enum RoutingListConfig {
+  instances = 'instances',
+  images = 'images',
+  connectedPlatforms = 'connected-platforms',
+  login = 'login',
+  billing = 'billing',
+  projects = 'projects',
+  users = 'users',
+  resources = 'resources',
+  audit = 'audit',
+  configuration = 'configuration',
+  swagger = 'swagger',
+  publickeyguide = 'help/publickeyguide',
+  accessnotebookguide = 'help/accessnotebookguide',
 }
diff --git a/services/self-service/src/main/resources/webapp/src/app/service-pages/service-pages.module.ts b/services/self-service/src/main/resources/webapp/src/app/core/pipes/normalize-link/index.ts
similarity index 73%
copy from services/self-service/src/main/resources/webapp/src/app/service-pages/service-pages.module.ts
copy to services/self-service/src/main/resources/webapp/src/app/core/pipes/normalize-link/index.ts
index f3da2fe6f..18c71d973 100644
--- a/services/self-service/src/main/resources/webapp/src/app/service-pages/service-pages.module.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/pipes/normalize-link/index.ts
@@ -19,12 +19,12 @@
 
 import { NgModule } from '@angular/core';
 import { CommonModule } from '@angular/common';
-import { NotFoundComponent } from './not-found/not-found.component';
-import { AccessDeniedComponent } from './access-denied/access-denied.component';
+import { NormalizeLinkPipe } from './normalize-link.pipe';
 
 @NgModule({
-  imports: [CommonModule],
-  declarations: [NotFoundComponent, AccessDeniedComponent],
-  exports: [NotFoundComponent, AccessDeniedComponent]
+  imports: [ CommonModule ],
+  declarations: [ NormalizeLinkPipe ],
+  exports: [ NormalizeLinkPipe ]
 })
-export class ServicePagesModule { }
\ No newline at end of file
+
+export class NormalizeLinkPipeModule { }
diff --git a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts b/services/self-service/src/main/resources/webapp/src/app/core/pipes/normalize-link/normalize-link.pipe.ts
similarity index 76%
copy from services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts
copy to services/self-service/src/main/resources/webapp/src/app/core/pipes/normalize-link/normalize-link.pipe.ts
index 34f860f16..ecbd3efcf 100644
--- a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/pipes/normalize-link/normalize-link.pipe.ts
@@ -17,11 +17,13 @@
  * under the License.
  */
 
-import { Component } from '@angular/core';
+import { Pipe, PipeTransform } from '@angular/core';
 
-@Component({
-    selector: 'not-found',
-    templateUrl: 'not-found.component.html',
-    styleUrls: ['not-found.component.scss']
+@Pipe({
+  name: 'normalizeLink'
 })
-export class NotFoundComponent { }
+export class NormalizeLinkPipe implements PipeTransform {
+  transform(value: string): string {
+    return value.includes('http') ? value : `//${value}`;
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/services/appRouting.service.ts b/services/self-service/src/main/resources/webapp/src/app/core/services/appRouting.service.ts
index 1a8075973..4eac9090f 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/services/appRouting.service.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/services/appRouting.service.ts
@@ -19,6 +19,7 @@
 
 import { Injectable } from '@angular/core';
 import { Router } from '@angular/router';
+import { RoutingListConfig } from '../configs/routing-list.config';
 
 @Injectable()
 export class AppRoutingService {
@@ -34,7 +35,7 @@ export class AppRoutingService {
   }
 
   redirectToHomePage(): void {
-    this.router.navigate(['/resources_list']);
+    this.router.navigate(['/', RoutingListConfig.instances]);
   }
 
   redirectToHealthStatusPage(): void {
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 3bb326d3e..e7218bc45 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
@@ -24,6 +24,7 @@ import { HttpClient } from '@angular/common/http';
 import { Dictionary } from '../collections';
 import { environment } from '../../../environments/environment';
 import { HTTPMethod } from '../util';
+import { AddPlatformFromValue } from '../../resources/connected-platforms/connected-platforms.models';
 
 // we can now access environment.apiUrl
 const API_URL = environment.apiUrl;
@@ -81,6 +82,7 @@ export class ApplicationServiceFacade {
   private static readonly AUDIT = 'audit';
   private static readonly CONFIG = 'config';
   private static readonly QUOTA = 'quota';
+  private static readonly CONNECTED_PLATFORMS = 'connected_platforms';
 
   private requestRegistry: Dictionary<string>;
 
@@ -180,6 +182,28 @@ export class ApplicationServiceFacade {
       null);
   }
 
+  buildGetConnectedPlatformsPage(): Observable<any> {
+    console.log(`${this.requestRegistry.Item(ApplicationServiceFacade.CONNECTED_PLATFORMS)}/user`);
+    return this.buildRequest(HTTPMethod.GET,
+      `${this.requestRegistry.Item(ApplicationServiceFacade.CONNECTED_PLATFORMS)}/user`,
+      null
+    );
+  }
+
+  buildAddPlatform(platformParams: AddPlatformFromValue): Observable<any> {
+    return this.buildRequest(HTTPMethod.POST,
+      this.requestRegistry.Item(ApplicationServiceFacade.CONNECTED_PLATFORMS),
+      platformParams
+    );
+  }
+
+  buildDisconnectPlatform(platformName: string): Observable<any> {
+    return this.buildRequest(HTTPMethod.DELETE,
+      `${this.requestRegistry.Item(ApplicationServiceFacade.CONNECTED_PLATFORMS)}/${platformName}`,
+      null
+    );
+  }
+
   public buildGetTemplatesRequest(params): Observable<any> {
     return this.buildRequest(HTTPMethod.GET,
       this.requestRegistry.Item(ApplicationServiceFacade.TEMPLATES) + params,
@@ -725,6 +749,10 @@ export class ApplicationServiceFacade {
     this.requestRegistry.Add(ApplicationServiceFacade.SCHEDULER,
       '/api/infrastructure_provision/exploratory_environment/scheduler');
 
+    // Connected Platforms
+    this.requestRegistry.Add(ApplicationServiceFacade.CONNECTED_PLATFORMS,
+      '/api/connected_platforms');
+
     // Computational Resources
     this.requestRegistry.Add(ApplicationServiceFacade.COMPUTATIONAL_RESOURCES,
       '/infrastructure_provision/computational_resources');
@@ -805,4 +833,4 @@ export class ApplicationServiceFacade {
       return this.http.get(body ? (url + body) : url, opt);
     }
   }
-}
\ No newline at end of file
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/services/connected-platform-api.service.ts b/services/self-service/src/main/resources/webapp/src/app/core/services/connected-platform-api.service.ts
new file mode 100644
index 000000000..bd7153080
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/core/services/connected-platform-api.service.ts
@@ -0,0 +1,54 @@
+/*!
+ * 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 { Injectable } from '@angular/core';
+import { ApplicationServiceFacade } from './applicationServiceFacade.service';
+import { catchError } from 'rxjs/operators';
+import { ErrorUtils } from '../util';
+import { Observable } from 'rxjs';
+import { AddPlatformFromValue, ConnectedPlatformsInfo } from '../../resources/connected-platforms/connected-platforms.models';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class ConnectedPlatformApiService {
+
+  constructor(
+    private applicationServiceFacade: ApplicationServiceFacade
+  ) { }
+
+  getConnectedPlatformsPage(): Observable<ConnectedPlatformsInfo> {
+    return this.applicationServiceFacade.buildGetConnectedPlatformsPage()
+      .pipe(
+        catchError(ErrorUtils.handleServiceError)
+      );
+  }
+
+  addPlatform(platformParams: AddPlatformFromValue) {
+    return  this.applicationServiceFacade.buildAddPlatform(platformParams).pipe(
+      catchError(ErrorUtils.handleServiceError)
+    );
+  }
+
+  disconnectPlatform(platformName: string) {
+    return  this.applicationServiceFacade.buildDisconnectPlatform(platformName).pipe(
+      catchError(ErrorUtils.handleServiceError)
+    );
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platform-dialog/connected-platform-dialog.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platform-dialog/connected-platform-dialog.component.html
new file mode 100644
index 000000000..9540c721f
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platform-dialog/connected-platform-dialog.component.html
@@ -0,0 +1,80 @@
+<!--
+  ~ 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 id="dialog-box" class="dialog__wrapper">
+    <header class="dialog-header">
+      <h4 class="modal-title">
+        {{modalTitle.addPlatform}}
+      </h4>
+      <button type="button" class="close" (click)="dialogRef.close()">&times;</button>
+    </header>
+    <div class="dialog-content">
+      <form [formGroup]="connectedPlatformForm">
+          <div class="control__wrapper">
+            <label>Select platform</label>
+            <mat-form-field class="select__wrapper" appearance="fill">
+              <mat-select placeholder="Select platform" name="platformType" formControlName="type">
+                <mat-option *ngIf="!data.types.length" disabled="true">
+                  No platforms to add
+                </mat-option><mat-option *ngFor="let type of data.types" [value]="type">
+                  {{type}}
+                </mat-option>
+              </mat-select>
+              <button class="caret">
+                <i class="material-icons">keyboard_arrow_down</i>
+              </button>
+            </mat-form-field>
+          </div>
+
+        <div class="control__wrapper">
+          <label>Platform url</label>
+          <div class="input__wrapper">
+            <input placeholder="Platform url" type="text" formControlName="url">
+            <span
+              class="error"
+              *ngIf="!connectedPlatformForm?.controls.url.valid
+                        && connectedPlatformForm.controls.url.touched"
+            >
+              Please provide a valid platform url.
+            </span>
+          </div>
+        </div>
+
+        <div class="control__wrapper">
+          <label>Name</label>
+          <div class="input__wrapper">
+            <input placeholder="Enter name" type="text" formControlName="name">
+            <span
+              class="error"
+              *ngIf="!connectedPlatformForm?.controls.name.valid && connectedPlatformForm.controls.name.touched"
+            >
+              Platform name cannot be longer than 6 characters.
+            </span>
+          </div>
+        </div>
+        <div class="button__wrapper">
+          <datalab-modal-btn
+            [isConfirmBtnDisabled]="!isFormValid"
+            [confirmBtnName]="confirmButtonName.add"
+            (closeEvent)="onBtnClick($event)"
+          ></datalab-modal-btn>
+        </div>
+      </form>
+    </div>
+  </div>
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platform-dialog/connected-platform-dialog.component.scss b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platform-dialog/connected-platform-dialog.component.scss
new file mode 100644
index 000000000..0becd0d19
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platform-dialog/connected-platform-dialog.component.scss
@@ -0,0 +1,98 @@
+/*!
+ * 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.
+ */
+
+.dialog-content {
+  padding: 25px 30px;
+}
+
+.control__wrapper {
+  display: flex;
+  justify-content: space-between;
+  align-items: baseline;
+  height: 56px;
+}
+
+.select__wrapper {
+  position: relative;
+  width: 320px;
+  height: 36px;
+
+  & ::ng-deep .mat-form-field-wrapper {
+    padding: 0;
+  }
+
+  &.mat-form-field .mat-form-field-flex {
+    background-color: red !important;
+  }
+
+  & ::ng-deep .mat-form-field-appearance-fill .mat-form-field-flex {
+    padding: 0;
+  }
+
+  & ::ng-deep .mat-form-field-infix {
+    border-top: none;
+  }
+
+  & ::ng-deep .mat-form-field-underline::before {
+    height: 0;
+  }
+
+  & ::ng-deep .mat-form-field-ripple {
+    display: none;
+  }
+
+  & ::ng-deep .mat-select-arrow {
+    border: none;
+
+  }
+
+  ::ng-deep .mat-select-placeholder {
+    font-size: 15px;
+  }
+}
+
+.caret {
+  position: absolute;
+  top: -9px;
+  right: -11px;
+  width: 40px;
+  height: 40px;
+  color: #35afd5;
+  background-color: transparent;
+  border: none;
+  border-left: 1px solid #ececec;
+  outline: none;
+  cursor: pointer;
+}
+
+::ng-deep {
+  .mat-form-field .mat-form-field-flex{
+    background-color: white;
+    box-shadow: 0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%);
+  }
+}
+
+.input__wrapper {
+  width: 320px;
+  height: 56px;
+}
+
+.button__wrapper {
+  padding-top: 20px;
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platform-dialog/connected-platform-dialog.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platform-dialog/connected-platform-dialog.component.ts
new file mode 100644
index 000000000..bf9f96e2e
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platform-dialog/connected-platform-dialog.component.ts
@@ -0,0 +1,70 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Component, Inject, OnInit } from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import { AddModalData, AddPlatformFromValue } from '../connected-platforms.models';
+import { ConfirmButtonNames } from '../connected-platforms.config';
+import { PATTERNS } from '../../../core/util';
+import { ModalTitle } from '../../../core/configs/connected-platforms.model';
+
+const URL_REGEXP_VALIDATION_STRING = '^(http(s)?)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]';
+
+@Component({
+  selector: 'datalab-connected-platform-dialog',
+  templateUrl: './connected-platform-dialog.component.html',
+  styleUrls: ['./connected-platform-dialog.component.scss']
+})
+export class ConnectedPlatformDialogComponent implements OnInit {
+  readonly modalTitle: typeof ModalTitle = ModalTitle;
+  readonly confirmButtonName: typeof ConfirmButtonNames = ConfirmButtonNames;
+
+  connectedPlatformForm!: FormGroup;
+
+  constructor(
+    public dialogRef: MatDialogRef<ConnectedPlatformDialogComponent>,
+    private fb: FormBuilder,
+    @Inject(MAT_DIALOG_DATA) public data: AddModalData
+  ) { }
+
+  ngOnInit(): void {
+    this.initForm();
+  }
+
+  onBtnClick(flag: boolean): void {
+    let responseObj: AddPlatformFromValue;
+    if (flag) {
+      responseObj = this.connectedPlatformForm.value;
+    }
+    this.dialogRef.close(responseObj);
+  }
+
+  private initForm(): void {
+    this.connectedPlatformForm = this.fb.group({
+      type: ['', Validators.required],
+      url: ['', [ Validators.required, Validators.pattern(URL_REGEXP_VALIDATION_STRING)]],
+      name: ['', [ Validators.required, Validators.pattern(PATTERNS.projectName), Validators.minLength(2)]]
+    });
+  }
+
+  get isFormValid(): boolean {
+    return this.connectedPlatformForm.valid;
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/service-pages/service-pages.module.ts b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms-routing.module.ts
similarity index 69%
copy from services/self-service/src/main/resources/webapp/src/app/service-pages/service-pages.module.ts
copy to services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms-routing.module.ts
index f3da2fe6f..61d93807f 100644
--- a/services/self-service/src/main/resources/webapp/src/app/service-pages/service-pages.module.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms-routing.module.ts
@@ -1,4 +1,4 @@
-/*
+/*!
  * 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
@@ -18,13 +18,18 @@
  */
 
 import { NgModule } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { NotFoundComponent } from './not-found/not-found.component';
-import { AccessDeniedComponent } from './access-denied/access-denied.component';
+import { RouterModule, Routes } from '@angular/router';
+import { ConnectedPlatformsComponent } from './connected-platforms.component';
+
+const routes: Routes = [
+  {
+    path: '',
+    component: ConnectedPlatformsComponent
+  }
+];
 
 @NgModule({
-  imports: [CommonModule],
-  declarations: [NotFoundComponent, AccessDeniedComponent],
-  exports: [NotFoundComponent, AccessDeniedComponent]
+  imports: [RouterModule.forChild(routes)],
+  exports: [RouterModule]
 })
-export class ServicePagesModule { }
\ No newline at end of file
+export class ConnectedPlatformsRoutingModule { }
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms.component.html
new file mode 100644
index 000000000..08acd3619
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms.component.html
@@ -0,0 +1,96 @@
+<!--
+  ~ 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.
+  -->
+
+<section class="connected-platforms__wrapper base-retreat">
+  <nav class="sub-nav">
+    <button
+      *ngIf="(connectedPlatformsStatus$ | async)?.add"
+      (click)="onAddNew()"
+      mat-raised-button
+      class="butt butt-create"
+    >
+      <i class="material-icons">add</i>Add new
+    </button>
+  </nav>
+
+  <mat-divider></mat-divider>
+
+  <table mat-table [dataSource]="(platformPageData$ | async).userPlatforms" class="mat-elevation-z8 table">
+
+    <ng-container matColumnDef="platformName">
+      <th mat-header-cell *matHeaderCellDef>{{tableHeaderCellTitles.platformName}}</th>
+      <td mat-cell class="column" *matCellDef="let element"> {{element.name}} </td>
+    </ng-container>
+
+    <ng-container matColumnDef="linkToPlatform">
+      <th mat-header-cell *matHeaderCellDef>{{tableHeaderCellTitles.linkToPlatform}}</th>
+      <td class="column" mat-cell *matCellDef="let element">
+        <a
+          class="link"
+          [href]="element.url | normalizeLink"
+          target="_blank"
+          [matTooltip]="element.url"
+          matTooltipPosition="above"
+          [matTooltipDisabled]="element.url.length < maxUrlLength"
+        >
+          {{element.url | truncateTextPipe : maxUrlLength}}
+        </a>
+      </td>
+    </ng-container>
+
+    <ng-container matColumnDef="actions">
+      <th
+        mat-header-cell
+        [ngClass]="{'hided-table-title': !(connectedPlatformsStatus$ | async)?.disconnect}"
+        *matHeaderCellDef
+      >
+        {{tableHeaderCellTitles.actions}}
+      </th>
+      <td mat-cell class="action-cell" *matCellDef="let element">
+        <span class="actions-menu"
+              #settings
+              (click)="actions.toggle($event, settings)">
+          <img
+            *ngIf="(connectedPlatformsStatus$ | async)?.disconnect"
+            class="action-icon"
+            [src]="'assets/svg/settings_icon.svg'"
+            alt="setting-icon"
+          >
+        </span>
+        <bubble-up #actions class="list-menu" position="bottom-left" alternative="top-left">
+          <ul class="list-unstyled">
+            <li>
+              <button
+                class="action-button__disconnect"
+                (click)="onPlatformDisconnect(element)"
+              >
+                <i class="material-icons icon">cast</i>
+                <span>Disconnect</span>
+              </button>
+            </li>
+          </ul>
+        </bubble-up>
+      </td>
+    </ng-container>
+
+    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
+    <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
+  </table>
+
+</section>
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms.component.scss b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms.component.scss
new file mode 100644
index 000000000..7dd04357d
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms.component.scss
@@ -0,0 +1,99 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+.table {
+  width: 100%;
+}
+
+.column {
+  width: 20%;
+}
+
+.action-icon,
+.link {
+  cursor: pointer;
+}
+
+.link {
+  text-decoration: underline;
+}
+
+.hided-table-title {
+  color: transparent;
+}
+
+.table-cell {
+  position: relative;
+}
+
+.list-menu {
+  left: auto;
+  top: 30px !important;
+  width: 190px;
+  max-height: calc(100vh / 2 - 70px);
+  margin-left: 0;
+  box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12);
+  border: none;
+}
+
+.actions-menu {
+  position: relative;
+  width: 190px;
+}
+
+.action-button__disconnect {
+  display: flex;
+  align-items: center;
+  width: 100%;
+  padding: 10px 15px;
+  background-color: transparent;
+  color: rgb(87, 114, 137);
+  border: none;
+  outline: none;
+
+  &:hover {
+    color: #35afd5;
+    cursor: pointer;
+
+    & ::before {
+      background-color: #35afd5;
+    }
+  }
+}
+
+.action-cell {
+  position: relative;
+}
+
+.icon {
+  position: relative;
+  margin-right: 10px;
+
+  &::before {
+    position: absolute;
+    top: 11px;
+    left: -4px;
+    width: 32px;
+    height: 1px;
+    rotate: 40deg;
+    content: '';
+    background-color: #577289;
+    transform: rotateX(1deg);
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms.component.ts
new file mode 100644
index 000000000..b49dbc545
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms.component.ts
@@ -0,0 +1,114 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Component, OnInit } from '@angular/core';
+import { BehaviorSubject, EMPTY, Observable, pipe } from 'rxjs';
+import { switchMap, take, tap } from 'rxjs/operators';
+import { MatDialog } from '@angular/material/dialog';
+import { ToastrService } from 'ngx-toastr';
+
+import { ConnectedPlatformsStatus, GeneralEnvironmentStatus } from '../../administration/management/management.model';
+import { HealthStatusService } from '../../core/services';
+import { ConnectedPlatformsTableTitles, ConnectedPlatformDisplayedColumns } from './connected-platforms.config';
+import { ConnectedPlatformDialogComponent } from './connected-platform-dialog/connected-platform-dialog.component';
+import { ConnectedPlatformsInfo, Platform } from './connected-platforms.models';
+import { ConnectedPlatformsService } from './connected-platforms.service';
+import { WarningDialogComponent } from './warning-dialog/warning-dialog.component';
+
+@Component({
+  selector: 'datalab-connected-platforms',
+  templateUrl: './connected-platforms.component.html',
+  styleUrls: ['./connected-platforms.component.scss']
+})
+export class ConnectedPlatformsComponent implements OnInit {
+  readonly tableHeaderCellTitles: typeof ConnectedPlatformsTableTitles = ConnectedPlatformsTableTitles;
+  readonly maxUrlLength: number = 30;
+
+  // tslint:disable-next-line:max-line-length
+  private readonly connectedPlatformsStatus$$: BehaviorSubject<ConnectedPlatformsStatus> = new BehaviorSubject<ConnectedPlatformsStatus>({} as ConnectedPlatformsStatus);
+  readonly connectedPlatformsStatus$: Observable<ConnectedPlatformsStatus> = this.connectedPlatformsStatus$$.asObservable();
+
+  platformPageData$: Observable<ConnectedPlatformsInfo>;
+
+  displayedColumns: typeof ConnectedPlatformDisplayedColumns = ConnectedPlatformDisplayedColumns;
+
+  constructor(
+    private healthStatusService: HealthStatusService,
+    public toastr: ToastrService,
+    private dialog: MatDialog,
+    private connectedPlatformsService: ConnectedPlatformsService
+  ) { }
+
+  ngOnInit(): void {
+    this.getEnvironmentHealthStatus();
+    this.getConnectedPlatformPageInfo();
+    this.initPageData();
+  }
+
+  onAddNew(): void {
+    this.dialog.open(ConnectedPlatformDialogComponent, {
+      data: this.connectedPlatformsService.addModalData,
+      panelClass: 'modal-lg'
+    })
+      .afterClosed()
+      .pipe(
+        this.getModalAction(this.connectedPlatformsService.addPlatform),
+      ).subscribe();
+  }
+
+  onPlatformDisconnect({name}: Platform): void {
+    this.dialog.open(WarningDialogComponent,
+      {
+        data: name,
+        panelClass: 'modal-sm'
+      })
+      .afterClosed()
+      .pipe(
+        this.getModalAction(this.connectedPlatformsService.disconnectPlatform)
+      ).subscribe();
+  }
+
+  private getModalAction(callback: Function) {
+    callback = callback.bind(this.connectedPlatformsService);
+    return pipe(
+      switchMap((arg) => {
+        if (arg) {
+          return callback(arg);
+        }
+        return EMPTY;
+      }),
+      switchMap(() => this.connectedPlatformsService.getConnectedPlatformPageInfo())
+    );
+  }
+
+  private getEnvironmentHealthStatus(): void {
+    this.healthStatusService.getEnvironmentHealthStatus().pipe(
+      tap((response: GeneralEnvironmentStatus) => this.connectedPlatformsStatus$$.next(response.connectedPlatforms)),
+      take(1)
+    ).subscribe();
+  }
+
+  private getConnectedPlatformPageInfo(): void {
+    this.connectedPlatformsService.getConnectedPlatformPageInfo().subscribe();
+  }
+
+  private initPageData(): void {
+    this.platformPageData$ = this.connectedPlatformsService.platformPageData$;
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.config.ts b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms.config.ts
similarity index 68%
copy from services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.config.ts
copy to services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms.config.ts
index 38df6d2c5..708ff528e 100644
--- a/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.config.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms.config.ts
@@ -17,19 +17,23 @@
  * under the License.
  */
 
-export const sideBarNamesConfig: Record<string, string> = {
-    resourses: 'Resources',
-    reports: 'Reports',
-    audit: 'Audit',
-    billing: 'Billing',
-    administration: 'Administration',
-    users: 'Users',
-    projects: 'Projects',
-    resources: 'Resources',
-    configuration: 'Configuration'
+export enum ConnectedPlatformsTableTitles {
+  platformName = 'Platform name',
+  linkToPlatform = 'Link to platform',
+  actions = 'Actions'
 }
 
-export interface UserInfo {
-    email: string;
-    name: string;
+export const ConnectedPlatformDisplayedColumns = [
+  'platformName',
+  'linkToPlatform',
+  'actions',
+];
+
+export enum ModalTitles {
+  disconnect= 'Disconnect platform'
+}
+
+export enum ConfirmButtonNames {
+  yes = 'Yes',
+  add = 'Add'
 }
diff --git a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms.models.ts
similarity index 69%
copy from services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts
copy to services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms.models.ts
index 34f860f16..d51487953 100644
--- a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms.models.ts
@@ -1,4 +1,4 @@
-/*
+/*!
  * 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
@@ -17,11 +17,19 @@
  * under the License.
  */
 
-import { Component } from '@angular/core';
+export interface ConnectedPlatformsInfo {
+  userPlatforms: Platform[];
+  types: string[];
+  platformNames: string[];
+}
 
-@Component({
-    selector: 'not-found',
-    templateUrl: 'not-found.component.html',
-    styleUrls: ['not-found.component.scss']
-})
-export class NotFoundComponent { }
+export interface Platform {
+  name: string;
+  type: string;
+  user: string;
+  url: string;
+}
+
+export type AddModalData = Omit<ConnectedPlatformsInfo, 'userPlatforms'>;
+
+export type AddPlatformFromValue = Omit<Platform, 'user'>;
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms.module.ts b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms.module.ts
new file mode 100644
index 000000000..1a653e50b
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms.module.ts
@@ -0,0 +1,53 @@
+/*!
+ * 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 { ConnectedPlatformsRoutingModule } from './connected-platforms-routing.module';
+import { ConnectedPlatformsComponent } from './connected-platforms.component';
+import { MaterialModule } from '../../shared/material.module';
+import { BubbleModule } from '../../shared';
+import { ReactiveFormsModule } from '@angular/forms';
+import { TruncateTextPipeModule } from '../../core/pipes/truncate-text-pipe';
+import { ModalPartsModule } from '../../shared/modal-parts/modal-parts.module';
+import { WarningDialogComponent } from './warning-dialog/warning-dialog.component';
+import { ConnectedPlatformDialogComponent } from './connected-platform-dialog/connected-platform-dialog.component';
+import { NormalizeLinkPipeModule } from '../../core/pipes/normalize-link';
+
+
+@NgModule({
+  declarations: [
+    ConnectedPlatformsComponent,
+    WarningDialogComponent,
+    ConnectedPlatformDialogComponent
+  ],
+  imports: [
+    CommonModule,
+    MaterialModule,
+    NormalizeLinkPipeModule,
+    ConnectedPlatformsRoutingModule,
+    BubbleModule,
+    ModalPartsModule,
+    ReactiveFormsModule,
+    TruncateTextPipeModule
+  ],
+  entryComponents: [ ConnectedPlatformDialogComponent, WarningDialogComponent ]
+})
+export class ConnectedPlatformsModule { }
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms.service.ts b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms.service.ts
new file mode 100644
index 000000000..14eea05b6
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/connected-platforms.service.ts
@@ -0,0 +1,61 @@
+/*!
+ * 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 { Injectable } from '@angular/core';
+import { ConnectedPlatformApiService } from '../../core/services/connected-platform-api.service';
+import { tap } from 'rxjs/operators';
+import { BehaviorSubject, Observable } from 'rxjs';
+import { AddModalData, AddPlatformFromValue, ConnectedPlatformsInfo } from './connected-platforms.models';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class ConnectedPlatformsService {
+  // tslint:disable-next-line:max-line-length
+  private platformPageData$$: BehaviorSubject<ConnectedPlatformsInfo> = new BehaviorSubject<ConnectedPlatformsInfo>({} as ConnectedPlatformsInfo);
+  platformPageData$: Observable<ConnectedPlatformsInfo> = this.platformPageData$$.asObservable();
+  addModalData: AddModalData;
+
+  constructor(
+    private connectedPlatformPageService: ConnectedPlatformApiService
+  ) { }
+
+  getConnectedPlatformPageInfo(): Observable<ConnectedPlatformsInfo> {
+    console.log(1);
+    return this.connectedPlatformPageService.getConnectedPlatformsPage()
+      .pipe(
+        tap(res => console.log(res)),
+        tap( result => this.platformPageData$$.next(result)),
+        tap( result => this.getAddModalData(result)),
+      );
+  }
+
+  addPlatform(platformParams: AddPlatformFromValue): Observable<any> {
+    return this.connectedPlatformPageService.addPlatform(platformParams);
+  }
+
+  disconnectPlatform(platformName: string): Observable<any> {
+    return this.connectedPlatformPageService.disconnectPlatform(platformName);
+  }
+
+  private getAddModalData(info: ConnectedPlatformsInfo): void {
+    const { platformNames, types } = info;
+    this.addModalData = { platformNames, types };
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/warning-dialog/warning-dialog.component.html
similarity index 67%
copy from services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.html
copy to services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/warning-dialog/warning-dialog.component.html
index 2eb5e74b3..32d382c05 100644
--- a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/warning-dialog/warning-dialog.component.html
@@ -17,17 +17,12 @@
   ~ under the License.
   -->
 
-<div class="not-found-page">
-  <div class="content">
-    <a class="logo" href="#/resources_list">
-      <img src="assets/svg/not_found_page.svg" alt="">
-    </a>
-
-    <div class="message-block">
-      <h3>Whooops... Page Not Found!</h3>
-      <p>We couldn't seem to found the page you are looking for.
-        <a href="#/resources_list">Go to the Homepage?</a>
-      </p>
-    </div>
-  </div>
+<datalab-modal-header (close)="onClose()" [modalTitle]="title.disconnect"></datalab-modal-header>
+<div class="dialog-content">
+  <p class="content red">The <span class="platform-name">{{data}}</span> will be disconnected from this account.</p>
+  <datalab-modal-btn
+    [needAdditionalQuestion]="true"
+    (closeEvent)="onBtnClick($event)"
+    [confirmBtnName]="confirmButtonName.yes"
+  ></datalab-modal-btn>
 </div>
diff --git a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/warning-dialog/warning-dialog.component.scss
similarity index 79%
copy from services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts
copy to services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/warning-dialog/warning-dialog.component.scss
index 34f860f16..310d4b09f 100644
--- a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/warning-dialog/warning-dialog.component.scss
@@ -1,4 +1,4 @@
-/*
+/*!
  * 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
@@ -17,11 +17,21 @@
  * under the License.
  */
 
-import { Component } from '@angular/core';
+.dialog-content {
+  padding: 25px 30px;
+}
 
-@Component({
-    selector: 'not-found',
-    templateUrl: 'not-found.component.html',
-    styleUrls: ['not-found.component.scss']
-})
-export class NotFoundComponent { }
+.red {
+  color: red;
+}
+
+.content {
+  margin-bottom: 50px;
+  padding-top: 20px;
+  text-align: center;
+  font-weight: 400;
+}
+
+.platform-name {
+  font-weight: 700;
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/warning-dialog/warning-dialog.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/warning-dialog/warning-dialog.component.ts
new file mode 100644
index 000000000..f30f0d3be
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/connected-platforms/warning-dialog/warning-dialog.component.ts
@@ -0,0 +1,49 @@
+/*!
+ * 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, Inject } from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+import { ConfirmButtonNames, ModalTitles } from '../connected-platforms.config';
+
+@Component({
+  selector: 'datalab-warning-dialog',
+  templateUrl: './warning-dialog.component.html',
+  styleUrls: ['./warning-dialog.component.scss']
+})
+export class WarningDialogComponent {
+  readonly title: typeof ModalTitles = ModalTitles;
+  readonly confirmButtonName: typeof ConfirmButtonNames = ConfirmButtonNames;
+
+  constructor(
+    public dialogRef: MatDialogRef<WarningDialogComponent>,
+    @Inject(MAT_DIALOG_DATA) public data: string
+  ) { }
+
+  onClose(): void {
+    this.dialogRef.close();
+  }
+
+  onBtnClick(isConfirm: boolean): void {
+    let platformName;
+    if (isConfirm) {
+      platformName = this.data;
+    }
+    this.dialogRef.close(platformName);
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/service-pages/access-denied/access-denied.component.ts b/services/self-service/src/main/resources/webapp/src/app/service-pages/access-denied/access-denied.component.ts
index cb290ec3c..b0075cac8 100644
--- a/services/self-service/src/main/resources/webapp/src/app/service-pages/access-denied/access-denied.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/service-pages/access-denied/access-denied.component.ts
@@ -17,20 +17,21 @@
  * under the License.
  */
 import { Component, OnInit } from '@angular/core';
+import { RoutingListConfig } from '../../core/configs/routing-list.config';
 
 @Component({
   selector: 'datalab-access-denied',
   template: `
     <div class="no-access-page">
       <div class="content">
-        <a class="logo" href="#/resources_list">
+        <a class="logo" [routerLink]="['/', routerList.instances]">
           <img src="assets/img/security-screen.png" alt="">
         </a>
 
         <div class="message-block">
           <h3>Access Denied!</h3>
           <p>The page you were trying to reach has restricted access.
-            <a href="#/resources_list">Go to the Homepage?</a>
+            <a [routerLink]="['/', routerList.instances]">Go to the Homepage?</a>
           </p>
         </div>
       </div>
@@ -38,7 +39,6 @@ import { Component, OnInit } from '@angular/core';
   `,
   styleUrls: ['./access-denied.component.scss']
 })
-export class AccessDeniedComponent implements OnInit {
-  constructor() { }
-  ngOnInit() { }
+export class AccessDeniedComponent {
+  readonly routerList: typeof RoutingListConfig = RoutingListConfig;
 }
diff --git a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.html b/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.html
index 2eb5e74b3..8dada3ee9 100644
--- a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.html
@@ -19,14 +19,14 @@
 
 <div class="not-found-page">
   <div class="content">
-    <a class="logo" href="#/resources_list">
+    <a class="logo" [routerLink]="['/', routerList.instances]">
       <img src="assets/svg/not_found_page.svg" alt="">
     </a>
 
     <div class="message-block">
       <h3>Whooops... Page Not Found!</h3>
       <p>We couldn't seem to found the page you are looking for.
-        <a href="#/resources_list">Go to the Homepage?</a>
+        <a [routerLink]="['/', routerList.instances]">Go to the Homepage?</a>
       </p>
     </div>
   </div>
diff --git a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts b/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts
index 34f860f16..08b17cd32 100644
--- a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts
@@ -18,10 +18,13 @@
  */
 
 import { Component } from '@angular/core';
+import { RoutingListConfig } from '../../core/configs/routing-list.config';
 
 @Component({
     selector: 'not-found',
     templateUrl: 'not-found.component.html',
     styleUrls: ['not-found.component.scss']
 })
-export class NotFoundComponent { }
+export class NotFoundComponent {
+  readonly routerList: typeof RoutingListConfig = RoutingListConfig;
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/service-pages/service-pages.module.ts b/services/self-service/src/main/resources/webapp/src/app/service-pages/service-pages.module.ts
index f3da2fe6f..268bf75e4 100644
--- a/services/self-service/src/main/resources/webapp/src/app/service-pages/service-pages.module.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/service-pages/service-pages.module.ts
@@ -21,10 +21,11 @@ import { NgModule } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { NotFoundComponent } from './not-found/not-found.component';
 import { AccessDeniedComponent } from './access-denied/access-denied.component';
+import { RouterModule } from '@angular/router';
 
 @NgModule({
-  imports: [CommonModule],
+  imports: [CommonModule, RouterModule],
   declarations: [NotFoundComponent, AccessDeniedComponent],
   exports: [NotFoundComponent, AccessDeniedComponent]
 })
-export class ServicePagesModule { }
\ No newline at end of file
+export class ServicePagesModule { }
diff --git a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.html b/services/self-service/src/main/resources/webapp/src/app/shared/modal-parts/modal-btn/modal-btn.component.html
similarity index 66%
copy from services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.html
copy to services/self-service/src/main/resources/webapp/src/app/shared/modal-parts/modal-btn/modal-btn.component.html
index 2eb5e74b3..0a5089c4b 100644
--- a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/modal-parts/modal-btn/modal-btn.component.html
@@ -16,18 +16,19 @@
   ~ specific language governing permissions and limitations
   ~ under the License.
   -->
-
-<div class="not-found-page">
-  <div class="content">
-    <a class="logo" href="#/resources_list">
-      <img src="assets/svg/not_found_page.svg" alt="">
-    </a>
-
-    <div class="message-block">
-      <h3>Whooops... Page Not Found!</h3>
-      <p>We couldn't seem to found the page you are looking for.
-        <a href="#/resources_list">Go to the Homepage?</a>
-      </p>
-    </div>
+<div class="component__wrapper">
+  <p *ngIf="needAdditionalQuestion" class="question center">
+    Do you want proceed?
+  </p>
+  <div class="button__wrapper">
+    <button type="button" class="butt mat-raised-button" (click)="close()">No</button>
+    <button
+      [disabled]="isConfirmBtnDisabled"
+      type="button"
+      class="butt butt-success mat-raised-button"
+      (click)="close(true)">
+      {{confirmBtnName}}
+    </button>
   </div>
 </div>
+
diff --git a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts b/services/self-service/src/main/resources/webapp/src/app/shared/modal-parts/modal-btn/modal-btn.component.scss
similarity index 79%
copy from services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts
copy to services/self-service/src/main/resources/webapp/src/app/shared/modal-parts/modal-btn/modal-btn.component.scss
index 34f860f16..d46002dc1 100644
--- a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/modal-parts/modal-btn/modal-btn.component.scss
@@ -17,11 +17,13 @@
  * under the License.
  */
 
-import { Component } from '@angular/core';
+.button__wrapper {
+  text-align: center;
+}
 
-@Component({
-    selector: 'not-found',
-    templateUrl: 'not-found.component.html',
-    styleUrls: ['not-found.component.scss']
-})
-export class NotFoundComponent { }
+.question {
+  margin-bottom: 20px;
+  text-align: center;
+  color: #718ba6;
+  font-weight: bold;
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts b/services/self-service/src/main/resources/webapp/src/app/shared/modal-parts/modal-btn/modal-btn.component.ts
similarity index 61%
copy from services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts
copy to services/self-service/src/main/resources/webapp/src/app/shared/modal-parts/modal-btn/modal-btn.component.ts
index 34f860f16..4aa51fb0c 100644
--- a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/modal-parts/modal-btn/modal-btn.component.ts
@@ -17,11 +17,21 @@
  * under the License.
  */
 
-import { Component } from '@angular/core';
+import { Component, EventEmitter, Input, Output } from '@angular/core';
 
 @Component({
-    selector: 'not-found',
-    templateUrl: 'not-found.component.html',
-    styleUrls: ['not-found.component.scss']
+  selector: 'datalab-modal-btn',
+  templateUrl: './modal-btn.component.html',
+  styleUrls: ['./modal-btn.component.scss']
 })
-export class NotFoundComponent { }
+export class ModalBtnComponent {
+  @Input() isConfirmBtnDisabled: boolean;
+  @Input() confirmBtnName: string;
+  @Input() needAdditionalQuestion: boolean = false;
+
+  @Output() closeEvent: EventEmitter<boolean> = new EventEmitter<boolean>();
+
+  close(flag: boolean = false): void {
+    this.closeEvent.emit(flag);
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.html b/services/self-service/src/main/resources/webapp/src/app/shared/modal-parts/modal-header/modal-header.component.html
similarity index 68%
copy from services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.html
copy to services/self-service/src/main/resources/webapp/src/app/shared/modal-parts/modal-header/modal-header.component.html
index 2eb5e74b3..83650e21b 100644
--- a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/modal-parts/modal-header/modal-header.component.html
@@ -17,17 +17,11 @@
   ~ under the License.
   -->
 
-<div class="not-found-page">
-  <div class="content">
-    <a class="logo" href="#/resources_list">
-      <img src="assets/svg/not_found_page.svg" alt="">
-    </a>
-
-    <div class="message-block">
-      <h3>Whooops... Page Not Found!</h3>
-      <p>We couldn't seem to found the page you are looking for.
-        <a href="#/resources_list">Go to the Homepage?</a>
-      </p>
-    </div>
-  </div>
+<div>
+  <header class="dialog-header">
+    <h4 class="modal-title">
+      {{modalTitle}}
+    </h4>
+    <button type="button" class="close" (click)="onClose()">&times;</button>
+  </header>
 </div>
diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.config.ts b/services/self-service/src/main/resources/webapp/src/app/shared/modal-parts/modal-header/modal-header.component.scss
similarity index 68%
copy from services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.config.ts
copy to services/self-service/src/main/resources/webapp/src/app/shared/modal-parts/modal-header/modal-header.component.scss
index 38df6d2c5..ece700481 100644
--- a/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.config.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/modal-parts/modal-header/modal-header.component.scss
@@ -1,4 +1,4 @@
-/*
+/*!
  * 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
@@ -17,19 +17,26 @@
  * under the License.
  */
 
-export const sideBarNamesConfig: Record<string, string> = {
-    resourses: 'Resources',
-    reports: 'Reports',
-    audit: 'Audit',
-    billing: 'Billing',
-    administration: 'Administration',
-    users: 'Users',
-    projects: 'Projects',
-    resources: 'Resources',
-    configuration: 'Configuration'
+.dialog-header {
+  position: relative;
+  padding-left: 30px;
+  height: 50px;
+  background: #f6fafe;
+  line-height: 50px;
 }
 
-export interface UserInfo {
-    email: string;
-    name: string;
+.close {
+  position: absolute;
+  top: 0;
+  right: 0;
+  height: 50px;
+  width: 50px;
+  font-size: 24px;
+  font-weight: 300;
+  border: 0;
+  background: none;
+  color: #577289;
+  outline: none;
+  cursor: pointer;
+  transition: all 0.5s ease-in-out;
 }
diff --git a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts b/services/self-service/src/main/resources/webapp/src/app/shared/modal-parts/modal-header/modal-header.component.ts
similarity index 67%
copy from services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts
copy to services/self-service/src/main/resources/webapp/src/app/shared/modal-parts/modal-header/modal-header.component.ts
index 34f860f16..7ef9b6d62 100644
--- a/services/self-service/src/main/resources/webapp/src/app/service-pages/not-found/not-found.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/modal-parts/modal-header/modal-header.component.ts
@@ -1,4 +1,4 @@
-/*
+/*!
  * 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
@@ -17,11 +17,20 @@
  * under the License.
  */
 
-import { Component } from '@angular/core';
+import { Component, EventEmitter, Input, Output } from '@angular/core';
 
 @Component({
-    selector: 'not-found',
-    templateUrl: 'not-found.component.html',
-    styleUrls: ['not-found.component.scss']
+  selector: 'datalab-modal-header',
+  templateUrl: './modal-header.component.html',
+  styleUrls: ['./modal-header.component.scss']
 })
-export class NotFoundComponent { }
+export class ModalHeaderComponent {
+  @Input() modalTitle: string;
+
+  @Output() close: EventEmitter<any> = new EventEmitter<any>();
+
+  onClose(): void {
+    this.close.emit();
+  }
+
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/service-pages/service-pages.module.ts b/services/self-service/src/main/resources/webapp/src/app/shared/modal-parts/modal-parts.module.ts
similarity index 72%
copy from services/self-service/src/main/resources/webapp/src/app/service-pages/service-pages.module.ts
copy to services/self-service/src/main/resources/webapp/src/app/shared/modal-parts/modal-parts.module.ts
index f3da2fe6f..3559594eb 100644
--- a/services/self-service/src/main/resources/webapp/src/app/service-pages/service-pages.module.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/modal-parts/modal-parts.module.ts
@@ -1,4 +1,4 @@
-/*
+/*!
  * 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
@@ -19,12 +19,22 @@
 
 import { NgModule } from '@angular/core';
 import { CommonModule } from '@angular/common';
-import { NotFoundComponent } from './not-found/not-found.component';
-import { AccessDeniedComponent } from './access-denied/access-denied.component';
+import { ModalHeaderComponent } from './modal-header/modal-header.component';
+import { ModalBtnComponent } from './modal-btn/modal-btn.component';
+
+
 
 @NgModule({
-  imports: [CommonModule],
-  declarations: [NotFoundComponent, AccessDeniedComponent],
-  exports: [NotFoundComponent, AccessDeniedComponent]
+  declarations: [
+    ModalHeaderComponent,
+    ModalBtnComponent
+  ],
+  imports: [
+    CommonModule
+  ],
+  exports: [
+    ModalBtnComponent,
+    ModalHeaderComponent
+  ]
 })
-export class ServicePagesModule { }
\ No newline at end of file
+export class ModalPartsModule { }
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 8c52c46f9..9ff4b3754 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,7 +28,7 @@
       <span class="line"></span>
     </button>
 
-    <a [routerLink]="['/resources_list']" class="navbar-logo">
+    <a [routerLink]="[routerList.instances]" class="navbar-logo">
       <img src="assets/svg/logo.svg" alt="">
     </a>
   </div>
@@ -37,11 +37,11 @@
     <!-- <a *ngIf="healthStatus.status" [routerLink]="['/environment_management']" class="statusbar">
       <span class="material-icons" ngClass="{{healthStatus.status || ''}}">radio_button_checked</span>
     </a> -->
-    
-    <a 
-      *ngIf="metadata" 
-      class="statusbar about-btn--wrapper" 
-      #info 
+
+    <a
+      *ngIf="metadata"
+      class="statusbar about-btn--wrapper"
+      #info
       (click)="actions.toggle($event, info)">
         <span class="about-btn">About</span>
     </a>
@@ -72,7 +72,7 @@
       <a class="help-link" href="https://github.com/apache/incubator-datalab/blob/master/USER_GUIDE.md" target="_blank">Help</a>
     </span>
 
-    <span 
+    <span
       class="material-icons account-icon--nav-bar account-icon" #login (click)="loginInfo.toggle($event, login)">
         account_circle
     </span>
@@ -85,53 +85,71 @@
         <span class="user-mail">{{userData.email}}</span>
         <button type="button" class="logout-btn" (click)="logout_btnClick()">
           Log out from account
-        </button> 
+        </button>
       </div>
     </bubble-up>
   </div>
 </div>
 
 <mat-sidenav-container class="example-container" autosize >
-  <mat-sidenav 
-    #drawer 
-    mode="side" 
-    opened 
-    role="navigation" 
-    [style.width]="isExpanded ? '220px' : '60px'" 
-    disableClose 
+  <mat-sidenav
+    #drawer
+    mode="side"
+    opened
+    role="navigation"
+    [style.width]="isExpanded ? '220px' : '60px'"
+    disableClose
     *ngIf="healthStatus"
   >
     <mat-nav-list >
       <nav>
         <div>
-          <a 
-            class="nav-item" 
-            [routerLink]="['/resources_list']" 
-            [routerLinkActive]="['active']"
-            [routerLinkActiveOptions]="{exact:true}"
-          >
-            <span *ngIf="isExpanded; else resources">{{sideBarNames.resourses}}</span>
-            <ng-template #resources><i class="material-icons">dashboard</i></ng-template>
+          <a class="nav-item has-children">
+            <span *ngIf="isExpanded">{{sideBarNames.resources}}</span>
+            <a
+              class="sub-nav-item"
+              [style.margin-left.px]="isExpanded ? '30' : '0'"
+              [routerLink]="[routerList.instances]"
+              [routerLinkActive]="['active']"
+              [routerLinkActiveOptions]="{exact:true}"
+            >
+              <span *ngIf="isExpanded; else instances">{{sideBarNames.instances}}</span>
+              <ng-template #instances><i class="material-icons">laptop</i></ng-template>
+            </a>
+
+            <a
+              *ngIf="healthStatus.connectedPlatforms?.view"
+              class="sub-nav-item"
+              [style.margin-left.px]="isExpanded ? '30' : '0'"
+              [routerLink]="[routerList.connectedPlatforms]"
+              [routerLinkActive]="['active']"
+              [routerLinkActiveOptions]="{exact:true}"
+            >
+              <span *ngIf="isExpanded; else connectedPlatforms">{{sideBarNames.connectedPlatforms}}</span>
+              <ng-template #connectedPlatforms><i class="material-icons">cast</i></ng-template>
+            </a>
+
           </a>
+
           <a class="nav-item has-children" *ngIf="healthStatus?.billingEnabled || healthStatus?.auditEnabled">
             <span *ngIf="isExpanded">{{sideBarNames.reports}}</span>
-            <a 
-              *ngIf="healthStatus?.auditEnabled" 
-              class="sub-nav-item" 
-              [routerLink]="['/audit']" 
+            <a
+              *ngIf="healthStatus?.auditEnabled"
+              class="sub-nav-item"
+              [routerLink]="['/audit']"
               [style.margin-left.px]="isExpanded ? '30' : '0'"
-              [routerLinkActive]="['active']" 
+              [routerLinkActive]="['active']"
               [routerLinkActiveOptions]="{exact:true}"
             >
               <span *ngIf="isExpanded; else audit">{{sideBarNames.audit}}</span>
               <ng-template #audit><i class="material-icons">library_books</i></ng-template>
             </a>
-            <a 
-              *ngIf="healthStatus?.billingEnabled" 
-              class="sub-nav-item" 
+            <a
+              *ngIf="healthStatus?.billingEnabled"
+              class="sub-nav-item"
               [routerLink]="['/billing_report']"
-              [routerLinkActive]="['active']" 
-              [routerLinkActiveOptions]="{exact:true}" 
+              [routerLinkActive]="['active']"
+              [routerLinkActiveOptions]="{exact:true}"
               [style.margin-left.px]="isExpanded ? '30' : '0'"
             >
               <span *ngIf="isExpanded; else billing">{{sideBarNames.billing}}</span>
@@ -141,21 +159,21 @@
           <a class="nav-item has-children" *ngIf="healthStatus?.admin || healthStatus?.projectAdmin">
             <span *ngIf="isExpanded">{{sideBarNames.administration}}</span>
 
-            <a 
-              class="sub-nav-item" 
-              [style.margin-left.px]="isExpanded ? '30' : '0'" 
+            <a
+              class="sub-nav-item"
+              [style.margin-left.px]="isExpanded ? '30' : '0'"
               [routerLink]="['/roles']"
-              [routerLinkActive]="['active']" 
+              [routerLinkActive]="['active']"
               [routerLinkActiveOptions]="{exact:true}"
             >
               <span *ngIf="isExpanded; else roles">{{sideBarNames.users}}</span>
               <ng-template #roles><i class="material-icons">account_box</i></ng-template>
             </a>
-            <a 
-              class="sub-nav-item" 
-              [style.margin-left.px]="isExpanded ? '30' : '0'" 
+            <a
+              class="sub-nav-item"
+              [style.margin-left.px]="isExpanded ? '30' : '0'"
               [routerLink]="['/projects']"
-              [routerLinkActive]="['active']" 
+              [routerLinkActive]="['active']"
               [routerLinkActiveOptions]="{exact:true}"
             >
               <span *ngIf="isExpanded; else projects">{{sideBarNames.projects}}</span>
@@ -166,21 +184,21 @@
             <!--              <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" 
+            <a
+              class="sub-nav-item"
               [style.margin-left.px]="isExpanded ? '30' : '0'"
-              [routerLink]="['/environment_management']" 
+              [routerLink]="['/environment_management']"
               [routerLinkActive]="['active']"
               [routerLinkActiveOptions]="{exact:true}"
             >
               <span *ngIf="isExpanded; else env">{{sideBarNames.resources}}</span>
               <ng-template #env><i class="material-icons">settings</i></ng-template>
             </a>
-            <a 
-              *ngIf="healthStatus?.admin" 
-              class="sub-nav-item" 
+            <a
+              *ngIf="healthStatus?.admin"
+              class="sub-nav-item"
               [style.margin-left.px]="isExpanded ? '30' : '0'"
-              [routerLink]="['/configuration']" 
+              [routerLink]="['/configuration']"
               [routerLinkActive]="['active']"
               [routerLinkActiveOptions]="{exact:true}"
             >
@@ -188,7 +206,7 @@
               <ng-template #env><i class="material-icons">build_circle</i></ng-template>
             </a>
           </a>
-          
+
         </div>
         <!--        <div>-->
         <!--          <a class="nav-item" [routerLink]="['/swagger']" [routerLinkActive]="['active']"-->
diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.component.ts b/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.component.ts
index b01184dc8..862f9feb5 100644
--- a/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.component.ts
@@ -23,12 +23,12 @@ import { Subscription, timer } from 'rxjs';
 import { ToastrService } from 'ngx-toastr';
 import { RouterOutlet } from '@angular/router';
 
-import { 
-  ApplicationSecurityService, 
-  HealthStatusService, 
-  AppRoutingService, 
-  SchedulerService, 
-  StorageService 
+import {
+  ApplicationSecurityService,
+  HealthStatusService,
+  AppRoutingService,
+  SchedulerService,
+  StorageService
 } from '../../core/services';
 import { GeneralEnvironmentStatus } from '../../administration/management/management.model';
 import { NotificationDialogComponent } from '../modal-dialog/notification-dialog';
@@ -37,12 +37,13 @@ import {
   animate,
   transition,
   style,
-  query, 
+  query,
   group,
 } from '@angular/animations';
 import {skip, take} from 'rxjs/operators';
 import {ProgressBarService} from '../../core/services/progress-bar.service';
-import{ sideBarNamesConfig, UserInfo } from './navbar.config'
+import { Sidebar_Names_Config, UserInfo } from './navbar.config';
+import { RoutingListConfig } from '../../core/configs/routing-list.config';
 
 interface Quota {
   projectQuotas: {};
@@ -85,6 +86,8 @@ export class NavbarComponent implements OnInit, OnDestroy {
 
   private readonly CHECK_ACTIVE_SCHEDULE_TIMEOUT: number = 300000;
   private readonly CHECK_ACTIVE_SCHEDULE_PERIOD: number = 15;
+  readonly routerList: typeof RoutingListConfig = RoutingListConfig;
+  readonly sideBarNames: typeof Sidebar_Names_Config = Sidebar_Names_Config;
 
   currentUserName: string;
   quotesLimit: number = 70;
@@ -93,7 +96,6 @@ export class NavbarComponent implements OnInit, OnDestroy {
   isExpanded: boolean = true;
   healthStatus: GeneralEnvironmentStatus;
   subscriptions: Subscription = new Subscription();
-  sideBarNames!: Record<string, string>;
   userData!: UserInfo;
   commitMaxLength: number = 22;
 
@@ -109,7 +111,6 @@ export class NavbarComponent implements OnInit, OnDestroy {
   ) { }
 
   ngOnInit() {
-    this.sideBarNames = sideBarNamesConfig;
     this.applicationSecurityService.loggedInStatus.subscribe(response => {
       this.subscriptions.unsubscribe();
       this.subscriptions.closed = false;
@@ -197,22 +198,22 @@ export class NavbarComponent implements OnInit, OnDestroy {
         } else {
           this.storage.setBillingQuoteUsed('');
         }
-        
+
         if (this.dialog.openDialogs.length > 0 || this.dialog.openDialogs.length > 0) return;
         checkQuotaAlert && this.emitQuotes(checkQuotaAlert, params.totalQuotaUsed, exceedProjects, informProjects);
       });
     }
   }
-  
+
   private getUserData(): UserInfo {
-    const token = localStorage.getItem('JWT_TOKEN')
+    const token = localStorage.getItem('JWT_TOKEN');
     const [_, tokenInfo] = token.split('.');
     const {name, email} = JSON.parse(atob(tokenInfo));
-    
+
     return {
       name: name || 'Jhon Doe',
       email: email || 'Email not found'
-    }
+    };
   }
 
   private checkAssignment(params): void {
diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.config.ts b/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.config.ts
index 38df6d2c5..7dfa42522 100644
--- a/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.config.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.config.ts
@@ -17,16 +17,18 @@
  * under the License.
  */
 
-export const sideBarNamesConfig: Record<string, string> = {
-    resourses: 'Resources',
-    reports: 'Reports',
-    audit: 'Audit',
-    billing: 'Billing',
-    administration: 'Administration',
-    users: 'Users',
-    projects: 'Projects',
-    resources: 'Resources',
-    configuration: 'Configuration'
+export enum Sidebar_Names_Config {
+  reports = 'Reports',
+  audit = 'Audit',
+  billing = 'Billing',
+  administration = 'Administration',
+  users = 'Users',
+  projects = 'Projects',
+  resources = 'Resources',
+  configuration = 'Configuration',
+  instances = 'Instances',
+  images = 'Images',
+  connectedPlatforms = 'Connected platforms'
 }
 
 export interface UserInfo {


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