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/02 09:58:39 UTC

[incubator-datalab] branch feat/DATALAB-2811/view-list-of-all-images created (now a2a18c6c5)

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

hshpak pushed a change to branch feat/DATALAB-2811/view-list-of-all-images
in repository https://gitbox.apache.org/repos/asf/incubator-datalab.git


      at a2a18c6c5 aaa

This branch includes the following new commits:

     new 7017c1f7c initial commit
     new 3289635e7 [feat/DATALAB-2811/view-list-of-all-images] added image page
     new a2a18c6c5 aaa

The 3 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/03: initial commit

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

hshpak pushed a commit to branch feat/DATALAB-2811/view-list-of-all-images
in repository https://gitbox.apache.org/repos/asf/incubator-datalab.git

commit 7017c1f7ceb4c28faf38f55c6361861afb3ae87b
Author: Hennadii_Shpak <bo...@gmail.com>
AuthorDate: Tue May 24 12:11:44 2022 +0300

    initial commit
---
 .../com/epam/datalab/model/exploratory/Image.java  |   3 +
 .../backendapi/dao/ImageExploratoryDAO.java        |   2 +
 .../backendapi/dao/ImageExploratoryDAOImpl.java    |  12 ++
 .../resources/ImageExploratoryResource.java        |  20 ++
 .../dto/{ImageInfoRecord.java => ImageFilter.java} |  38 +++-
 .../backendapi/resources/dto/ImageInfoRecord.java  |   7 +
 .../resources/dto/ProjectImagesInfo.java}          |  32 +--
 .../service/ImageExploratoryService.java           |   5 +
 .../service/impl/ImageExploratoryServiceImpl.java  |  35 +++
 .../resources/webapp/src/app/app.routing.module.ts |  32 +--
 .../resources/webapp/src/app/core/core.module.ts   |   7 +-
 .../capitalize-first-letter.pipe.ts}               |  26 ++-
 .../{ => capitalize-first-letter-pipe}/index.ts    |  17 +-
 .../resources/webapp/src/app/core/pipes/index.ts   |   1 +
 .../src/app/core/services/appRouting.service.ts    |   2 +-
 .../services/applicationServiceFacade.service.ts   |  21 +-
 .../webapp/src/app/core/services/index.ts          |   1 +
 .../app/core/services/user-images-page.service.ts} |  32 +--
 .../src/app/core/services/userResource.service.ts  |  15 +-
 .../reporting-grid/reporting-grid.component.ts     |  15 +-
 .../bucket-browser/bucket-browser.component.ts     |  17 +-
 .../src/app/resources/images/images.component.html | 240 +++++++++++++++++++++
 .../src/app/resources/images/images.component.scss | 102 +++++++++
 .../src/app/resources/images/images.component.ts   | 135 ++++++++++++
 .../src/app/resources/images/images.config.ts      |  10 +
 .../src/app/resources/images/images.model.ts       |  20 ++
 .../webapp/src/app/resources/images/index.ts       |   2 +
 .../resources-grid/resources-grid.component.ts     |   9 +-
 .../resources-grid/resources-grid.model.ts         |   7 -
 .../src/app/resources/resources.component.html     |  28 +--
 .../src/app/resources/resources.component.ts       |   2 +-
 .../webapp/src/app/resources/resources.module.ts   |  10 +-
 .../src/app/shared/bubble/bubble.component.ts      |  29 +--
 .../src/app/shared/navbar/navbar.component.html    | 113 +++++-----
 .../src/app/shared/navbar/navbar.component.ts      |   5 +-
 .../webapp/src/app/shared/navbar/navbar.config.ts  |  25 +--
 .../src/main/resources/webapp/src/styles.scss      |   1 +
 .../resources/ImageExploratoryResourceTest.java    |  16 +-
 .../service/impl/BillingServiceImplTest.java       |  15 +-
 .../impl/ImageExploratoryServiceImplTest.java      |   5 +-
 40 files changed, 900 insertions(+), 214 deletions(-)

diff --git a/services/datalab-model/src/main/java/com/epam/datalab/model/exploratory/Image.java b/services/datalab-model/src/main/java/com/epam/datalab/model/exploratory/Image.java
index c8d3b2c35..322d5511a 100644
--- a/services/datalab-model/src/main/java/com/epam/datalab/model/exploratory/Image.java
+++ b/services/datalab-model/src/main/java/com/epam/datalab/model/exploratory/Image.java
@@ -24,6 +24,7 @@ import com.epam.datalab.model.library.Library;
 import lombok.Builder;
 import lombok.Data;
 
+import java.time.Instant;
 import java.util.List;
 import java.util.Map;
 
@@ -40,6 +41,8 @@ public class Image {
     private final String fullName;
     private final String externalName;
     private final String application;
+    private final String instanceName;
+    private final String cloudProvider;
     private final String dockerImage;
     private final List<Library> libraries;
     private final Map<String, List<Library>> computationalLibraries;
diff --git a/services/self-service/src/main/java/com/epam/datalab/backendapi/dao/ImageExploratoryDAO.java b/services/self-service/src/main/java/com/epam/datalab/backendapi/dao/ImageExploratoryDAO.java
index d8e89874a..4d97437e6 100644
--- a/services/self-service/src/main/java/com/epam/datalab/backendapi/dao/ImageExploratoryDAO.java
+++ b/services/self-service/src/main/java/com/epam/datalab/backendapi/dao/ImageExploratoryDAO.java
@@ -38,6 +38,8 @@ public interface ImageExploratoryDAO {
 
     List<ImageInfoRecord> getImages(String user, String dockerImage, String project, String endpoint, ImageStatus... statuses);
 
+    List<ImageInfoRecord> getImagesOfUser(String user, String project);
+
     List<ImageInfoRecord> getImagesForProject(String project);
 
     Optional<ImageInfoRecord> getImage(String user, String name, String project, String endpoint);
diff --git a/services/self-service/src/main/java/com/epam/datalab/backendapi/dao/ImageExploratoryDAOImpl.java b/services/self-service/src/main/java/com/epam/datalab/backendapi/dao/ImageExploratoryDAOImpl.java
index 234a15e52..95f4e0a1a 100644
--- a/services/self-service/src/main/java/com/epam/datalab/backendapi/dao/ImageExploratoryDAOImpl.java
+++ b/services/self-service/src/main/java/com/epam/datalab/backendapi/dao/ImageExploratoryDAOImpl.java
@@ -54,6 +54,7 @@ public class ImageExploratoryDAOImpl extends BaseDAO implements ImageExploratory
     private static final String DOCKER_IMAGE = "dockerImage";
     private static final String PROJECT = "project";
     private static final String ENDPOINT = "endpoint";
+    private static final String CREATION_DATE = "creationDate";
 
     @Override
     public boolean exist(String image, String project) {
@@ -79,6 +80,13 @@ public class ImageExploratoryDAOImpl extends BaseDAO implements ImageExploratory
                 ImageInfoRecord.class);
     }
 
+    @Override
+    public List<ImageInfoRecord> getImagesOfUser(String user, String project) {
+        return find(MongoCollections.IMAGES,
+                imageUserProjectCondition(user, project),
+                ImageInfoRecord.class);
+    }
+
     @Override
     public List<ImageInfoRecord> getImagesForProject(String project) {
         return find(MongoCollections.IMAGES,
@@ -146,6 +154,10 @@ public class ImageExploratoryDAOImpl extends BaseDAO implements ImageExploratory
         return and(eq(IMAGE_NAME, image), eq(PROJECT, project));
     }
 
+    private Bson imageUserProjectCondition(String user, String project) {
+        return and(eq(USER, user), eq(PROJECT, project));
+    }
+
     private Document getUpdatedFields(Image image) {
         return new Document(STATUS, image.getStatus().toString())
                 .append(IMAGE_FULL_NAME, image.getFullName())
diff --git a/services/self-service/src/main/java/com/epam/datalab/backendapi/resources/ImageExploratoryResource.java b/services/self-service/src/main/java/com/epam/datalab/backendapi/resources/ImageExploratoryResource.java
index 95dcdb469..8c5c76861 100644
--- a/services/self-service/src/main/java/com/epam/datalab/backendapi/resources/ImageExploratoryResource.java
+++ b/services/self-service/src/main/java/com/epam/datalab/backendapi/resources/ImageExploratoryResource.java
@@ -22,7 +22,9 @@ package com.epam.datalab.backendapi.resources;
 import com.epam.datalab.auth.UserInfo;
 import com.epam.datalab.backendapi.domain.RequestId;
 import com.epam.datalab.backendapi.resources.dto.ExploratoryImageCreateFormDTO;
+import com.epam.datalab.backendapi.resources.dto.ImageFilter;
 import com.epam.datalab.backendapi.resources.dto.ImageInfoRecord;
+import com.epam.datalab.backendapi.resources.dto.ProjectImagesInfo;
 import com.epam.datalab.backendapi.service.ImageExploratoryService;
 import com.google.inject.Inject;
 import io.dropwizard.auth.Auth;
@@ -98,6 +100,24 @@ public class ImageExploratoryResource {
         return Response.ok(images).build();
     }
 
+
+    @GET
+    @Path("user")
+    public Response getImagesForUser(@Auth UserInfo ui) {
+        log.debug("Getting images for user {}", ui.getName());
+        final List<ProjectImagesInfo> images = imageExploratoryService.getImagesOfUser(ui);
+        return Response.ok(images).build();
+    }
+
+    @POST
+    @Path("user")
+    public Response getImagesForUser(@Auth UserInfo ui, @Valid @NotNull ImageFilter imageFilter) {
+        log.debug("Getting images for user {} with filter {}", ui.getName(), imageFilter);
+        final List<ProjectImagesInfo> images = imageExploratoryService.getImagesOfUserWithFilter(ui ,imageFilter);
+        return Response.ok(images).build();
+    }
+
+
     @GET
     @Path("{name}")
     public Response getImage(@Auth UserInfo ui,
diff --git a/services/self-service/src/main/java/com/epam/datalab/backendapi/resources/dto/ImageInfoRecord.java b/services/self-service/src/main/java/com/epam/datalab/backendapi/resources/dto/ImageFilter.java
similarity index 58%
copy from services/self-service/src/main/java/com/epam/datalab/backendapi/resources/dto/ImageInfoRecord.java
copy to services/self-service/src/main/java/com/epam/datalab/backendapi/resources/dto/ImageFilter.java
index 18692c34c..09c50ef58 100644
--- a/services/self-service/src/main/java/com/epam/datalab/backendapi/resources/dto/ImageInfoRecord.java
+++ b/services/self-service/src/main/java/com/epam/datalab/backendapi/resources/dto/ImageFilter.java
@@ -19,19 +19,39 @@
 
 package com.epam.datalab.backendapi.resources.dto;
 
+import com.epam.datalab.cloud.CloudProvider;
 import com.epam.datalab.dto.exploratory.ImageStatus;
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
 import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.NonNull;
+
+import java.util.List;
 
 @Data
+@NoArgsConstructor
 @JsonIgnoreProperties(ignoreUnknown = true)
-public class ImageInfoRecord {
-    private final String name;
-    private final String description;
-    private final String project;
-    private final String endpoint;
-    private final String user;
-    private final String application;
-    private final String fullName;
-    private final ImageStatus status;
+public class ImageFilter {
+    @NonNull
+    private String imageName;
+    @NonNull
+    @JsonProperty("date_start")
+    private String dateStart;
+    @NonNull
+    @JsonProperty("date_end")
+    private String dateEnd;
+    @NonNull
+    private List<CloudProvider> cloudProviders;
+    @NonNull
+    private List<ImageStatus> statuses;
+//    @NonNull
+//    private List<> sharingStatuses;
+    @NonNull
+    private List<String> templateNames;
+    @NonNull
+    private List<String> instanceNames;
+    @NonNull
+    private List<String> projects;
+
 }
diff --git a/services/self-service/src/main/java/com/epam/datalab/backendapi/resources/dto/ImageInfoRecord.java b/services/self-service/src/main/java/com/epam/datalab/backendapi/resources/dto/ImageInfoRecord.java
index 18692c34c..00e5c94c6 100644
--- a/services/self-service/src/main/java/com/epam/datalab/backendapi/resources/dto/ImageInfoRecord.java
+++ b/services/self-service/src/main/java/com/epam/datalab/backendapi/resources/dto/ImageInfoRecord.java
@@ -19,19 +19,26 @@
 
 package com.epam.datalab.backendapi.resources.dto;
 
+import com.epam.datalab.cloud.CloudProvider;
 import com.epam.datalab.dto.exploratory.ImageStatus;
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import lombok.Data;
 
+import java.time.LocalDateTime;
+
 @Data
 @JsonIgnoreProperties(ignoreUnknown = true)
 public class ImageInfoRecord {
     private final String name;
+    private final String creationDate;
     private final String description;
     private final String project;
     private final String endpoint;
     private final String user;
     private final String application;
+    private final String instanceName;
+    private final CloudProvider cloudProvider;
     private final String fullName;
     private final ImageStatus status;
+    private final String sharedStatus;
 }
diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.config.ts b/services/self-service/src/main/java/com/epam/datalab/backendapi/resources/dto/ProjectImagesInfo.java
similarity index 64%
copy from services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.config.ts
copy to services/self-service/src/main/java/com/epam/datalab/backendapi/resources/dto/ProjectImagesInfo.java
index 38df6d2c5..06da51f28 100644
--- a/services/self-service/src/main/resources/webapp/src/app/shared/navbar/navbar.config.ts
+++ b/services/self-service/src/main/java/com/epam/datalab/backendapi/resources/dto/ProjectImagesInfo.java
@@ -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'
-}
+package com.epam.datalab.backendapi.resources.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.util.List;
 
-export interface UserInfo {
-    email: string;
-    name: string;
+@AllArgsConstructor
+@Builder
+@EqualsAndHashCode
+@ToString
+public class ProjectImagesInfo {
+    @JsonProperty
+    private String project;
+    @JsonProperty
+    private List<ImageInfoRecord> images;
 }
diff --git a/services/self-service/src/main/java/com/epam/datalab/backendapi/service/ImageExploratoryService.java b/services/self-service/src/main/java/com/epam/datalab/backendapi/service/ImageExploratoryService.java
index fae72a33f..99973f1a6 100644
--- a/services/self-service/src/main/java/com/epam/datalab/backendapi/service/ImageExploratoryService.java
+++ b/services/self-service/src/main/java/com/epam/datalab/backendapi/service/ImageExploratoryService.java
@@ -20,7 +20,9 @@
 package com.epam.datalab.backendapi.service;
 
 import com.epam.datalab.auth.UserInfo;
+import com.epam.datalab.backendapi.resources.dto.ImageFilter;
 import com.epam.datalab.backendapi.resources.dto.ImageInfoRecord;
+import com.epam.datalab.backendapi.resources.dto.ProjectImagesInfo;
 import com.epam.datalab.model.exploratory.Image;
 
 import java.util.List;
@@ -36,4 +38,7 @@ public interface ImageExploratoryService {
     ImageInfoRecord getImage(String user, String name, String project, String endpoint);
 
     List<ImageInfoRecord> getImagesForProject(String project);
+
+    List<ProjectImagesInfo> getImagesOfUser(UserInfo user);
+    List<ProjectImagesInfo> getImagesOfUserWithFilter(UserInfo user, ImageFilter imageFilter);
 }
diff --git a/services/self-service/src/main/java/com/epam/datalab/backendapi/service/impl/ImageExploratoryServiceImpl.java b/services/self-service/src/main/java/com/epam/datalab/backendapi/service/impl/ImageExploratoryServiceImpl.java
index 8c6802167..c0cd6e54d 100644
--- a/services/self-service/src/main/java/com/epam/datalab/backendapi/service/impl/ImageExploratoryServiceImpl.java
+++ b/services/self-service/src/main/java/com/epam/datalab/backendapi/service/impl/ImageExploratoryServiceImpl.java
@@ -30,7 +30,9 @@ import com.epam.datalab.backendapi.dao.ExploratoryLibDAO;
 import com.epam.datalab.backendapi.dao.ImageExploratoryDAO;
 import com.epam.datalab.backendapi.domain.EndpointDTO;
 import com.epam.datalab.backendapi.domain.ProjectDTO;
+import com.epam.datalab.backendapi.resources.dto.ImageFilter;
 import com.epam.datalab.backendapi.resources.dto.ImageInfoRecord;
+import com.epam.datalab.backendapi.resources.dto.ProjectImagesInfo;
 import com.epam.datalab.backendapi.service.EndpointService;
 import com.epam.datalab.backendapi.service.ImageExploratoryService;
 import com.epam.datalab.backendapi.service.ProjectService;
@@ -53,6 +55,7 @@ import com.google.inject.Singleton;
 import com.google.inject.name.Named;
 import lombok.extern.slf4j.Slf4j;
 
+import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Predicate;
@@ -104,6 +107,8 @@ public class ImageExploratoryServiceImpl implements ImageExploratoryService {
                 .computationalLibraries(fetchComputationalLibs(libraries))
                 .dockerImage(userInstance.getImageName())
                 .exploratoryId(userInstance.getId())
+                .instanceName(userInstance.getExploratoryName())
+                .cloudProvider(userInstance.getCloudProvider())
                 .project(userInstance.getProject())
                 .endpoint(userInstance.getEndpoint())
                 .build());
@@ -154,6 +159,36 @@ public class ImageExploratoryServiceImpl implements ImageExploratoryService {
         return imageExploratoryDao.getImagesForProject(project);
     }
 
+    @Override
+    public List<ProjectImagesInfo> getImagesOfUser(UserInfo user) {
+        log.debug("Loading list of images for user {}", user.getName());
+        return projectService.getUserProjects(user, Boolean.FALSE)
+                .stream()
+                .map( p-> {
+                    List<ImageInfoRecord> images =  imageExploratoryDao.getImagesOfUser(user.getName(), p.getName());
+                    return ProjectImagesInfo.builder()
+                            .project(p.getName())
+                            .images(images)
+                            .build();
+                })
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public List<ProjectImagesInfo> getImagesOfUserWithFilter(UserInfo user, ImageFilter imageFilter) {
+        log.debug("Loading list of images for user {}", user.getName());
+        return projectService.getUserProjects(user, Boolean.FALSE)
+                .stream()
+                .map( p-> {
+                    List<ImageInfoRecord> images =  imageExploratoryDao.getImagesOfUser(user.getName(), p.getName());
+                    return ProjectImagesInfo.builder()
+                            .project(p.getName())
+                            .images(images)
+                            .build();
+                })
+                .collect(Collectors.toList());
+    }
+
     private Map<String, List<Library>> fetchComputationalLibs(List<Library> libraries) {
         return libraries.stream()
                 .filter(resourceTypePredicate(ResourceType.COMPUTATIONAL))
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..af905a56e 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 {ImagesComponent} from './resources/images/images.component';
 
 const routes: Routes = [
   {
     path: 'login',
     component: LoginComponent
-  }, 
+  },
   {
     path: '',
     canActivate: [CheckParamsGuard],
@@ -50,19 +51,24 @@ const routes: Routes = [
     children: [
       {
         path: '',
-        redirectTo: 'resources_list',
+        redirectTo: 'instances',
         pathMatch: 'full'
-      }, 
+      },
       {
-        path: 'resources_list',
+        path: 'instances',
         component: ResourcesComponent,
         canActivate: [AuthorizationGuard]
-      }, 
+      },
+      {
+        path: 'images',
+        component: ImagesComponent,
+        canActivate: [AuthorizationGuard]
+      },
       {
         path: 'billing_report',
         component: ReportingComponent,
         canActivate: [AuthorizationGuard, CloudProviderGuard]
-      }, 
+      },
       {
         path: 'projects',
         component: ProjectComponent,
@@ -76,12 +82,12 @@ const routes: Routes = [
         path: 'roles',
         component: RolesComponent,
         canActivate: [AuthorizationGuard, AdminGuard],
-      }, 
+      },
       {
         path: 'environment_management',
         component: ManagementComponent,
         canActivate: [AuthorizationGuard, AdminGuard]
-      }, 
+      },
       {
         path: 'configuration',
         component: ConfigurationComponent,
@@ -91,12 +97,12 @@ const routes: Routes = [
         path: 'swagger',
         component: SwaggerComponent,
         canActivate: [AuthorizationGuard]
-      }, 
+      },
       {
         path: 'help/publickeyguide',
         component: PublicKeyGuideComponent,
         canActivate: [AuthorizationGuard]
-      }, 
+      },
       {
         path: 'help/accessnotebookguide',
         component: AccessNotebookGuideComponent,
@@ -108,16 +114,16 @@ const routes: Routes = [
         canActivate: [AuthorizationGuard, AuditGuard],
       },
     ]
-  }, 
+  },
   {
     path: 'terminal/:id/:endpoint',
     component: WebterminalComponent
-  }, 
+  },
   {
     path: '403',
     component: AccessDeniedComponent,
     canActivate: [AuthorizationGuard]
-  }, 
+  },
   {
   path: '**',
   component: NotFoundComponent
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/core.module.ts b/services/self-service/src/main/resources/webapp/src/app/core/core.module.ts
index 20a45126c..33dc379fd 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/core.module.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/core.module.ts
@@ -48,9 +48,9 @@ import { NoCacheInterceptor } from './interceptors/nocache.interceptor';
 import { ErrorInterceptor } from './interceptors/error.interceptor';
 
 import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
-import {ConfigurationService} from './services/configutration.service';
-import {AuditGuard, OdahuDeploymentService} from './services';
-import {ProjectAdminGuard} from './services/projectAdmin.guard';
+import {  ConfigurationService } from './services/configutration.service';
+import {  AuditGuard, OdahuDeploymentService, UserImagesPageService } from './services';
+import {  ProjectAdminGuard } from './services/projectAdmin.guard';
 
 @NgModule({
   imports: [CommonModule],
@@ -90,6 +90,7 @@ export class CoreModule {
         UserAccessKeyService,
         ConfigurationService,
         OdahuDeploymentService,
+        UserImagesPageService,
 
         { provide: MatDialogRef, useValue: {} },
         { provide: MAT_DIALOG_DATA, useValue: [] },
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/pipes/capitalize-first-letter-pipe/capitalize-first-letter.pipe.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/core/pipes/capitalize-first-letter-pipe/capitalize-first-letter.pipe.ts
index 38df6d2c5..df57fee83 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/pipes/capitalize-first-letter-pipe/capitalize-first-letter.pipe.ts
@@ -17,19 +17,17 @@
  * 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;
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({ name: 'capitalizeFirstLetter' })
+
+export class CapitalizeFirstLetterPipe implements PipeTransform {
+  transform(value: string): string {
+    if (!value) {
+      return '';
+    }
+    const firstLetter = value. substring(0, 1). toUpperCase();
+    return `${firstLetter}${value.substring(1).toLowerCase()}`;
+  }
 }
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/pipes/index.ts b/services/self-service/src/main/resources/webapp/src/app/core/pipes/capitalize-first-letter-pipe/index.ts
similarity index 70%
copy from services/self-service/src/main/resources/webapp/src/app/core/pipes/index.ts
copy to services/self-service/src/main/resources/webapp/src/app/core/pipes/capitalize-first-letter-pipe/index.ts
index 399ce1d31..d333ad504 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/pipes/index.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/pipes/capitalize-first-letter-pipe/index.ts
@@ -17,9 +17,14 @@
  * under the License.
  */
 
-export * from './keys-pipe';
-export * from './underscoreless-pipe';
-export * from './lib-sort-pipe';
-export * from './replace-breaks-pipe';
-export * from './highlight.pipe';
-export * from './convert-action-pipe';
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { CapitalizeFirstLetterPipe } from './capitalize-first-letter.pipe';
+
+@NgModule({
+  imports: [CommonModule],
+  declarations: [CapitalizeFirstLetterPipe],
+  exports: [CapitalizeFirstLetterPipe]
+})
+
+export class CapitalizeFirstLetterPipeModule { }
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/pipes/index.ts b/services/self-service/src/main/resources/webapp/src/app/core/pipes/index.ts
index 399ce1d31..bc2e2c7b6 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/pipes/index.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/pipes/index.ts
@@ -23,3 +23,4 @@ export * from './lib-sort-pipe';
 export * from './replace-breaks-pipe';
 export * from './highlight.pipe';
 export * from './convert-action-pipe';
+export * from './capitalize-first-letter-pipe';
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..3dd07cbd7 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
@@ -34,7 +34,7 @@ export class AppRoutingService {
   }
 
   redirectToHomePage(): void {
-    this.router.navigate(['/resources_list']);
+    this.router.navigate(['/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 c4fb49bd7..cd39b0e67 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
@@ -17,13 +17,13 @@
  * under the License.
  */
 
-import { Injectable } from '@angular/core';
-import { Observable } from 'rxjs';
-import { HttpClient } from '@angular/common/http';
+import {Injectable} from '@angular/core';
+import {Observable} from 'rxjs';
+import {HttpClient} from '@angular/common/http';
 
-import { Dictionary } from '../collections';
-import { environment } from '../../../environments/environment';
-import { HTTPMethod } from '../util';
+import {Dictionary} from '../collections';
+import {environment} from '../../../environments/environment';
+import {HTTPMethod} from '../util';
 
 // we can now access environment.apiUrl
 const API_URL = environment.apiUrl;
@@ -81,6 +81,7 @@ export class ApplicationServiceFacade {
   private static readonly AUDIT = 'audit';
   private static readonly CONFIG = 'config';
   private static readonly QUOTA = 'quota';
+  private static readonly IMAGE_PAGE = 'image_page';
 
   private requestRegistry: Dictionary<string>;
 
@@ -180,6 +181,12 @@ export class ApplicationServiceFacade {
       null);
   }
 
+  buildGetUserImagePage(): Observable<any> {
+    return this.buildRequest(HTTPMethod.GET,
+      this.requestRegistry.Item(ApplicationServiceFacade.IMAGE_PAGE),
+      null);
+  }
+
   public buildGetTemplatesRequest(params): Observable<any> {
     return this.buildRequest(HTTPMethod.GET,
       this.requestRegistry.Item(ApplicationServiceFacade.TEMPLATES) + params,
@@ -714,6 +721,8 @@ export class ApplicationServiceFacade {
     // Exploratory Environment
     this.requestRegistry.Add(ApplicationServiceFacade.PROVISIONED_RESOURCES,
       '/api/infrastructure/info');
+    this.requestRegistry.Add(ApplicationServiceFacade.IMAGE_PAGE,
+      '/api/infrastructure_provision/exploratory_environment/image/user');
     this.requestRegistry.Add(ApplicationServiceFacade.EXPLORATORY_ENVIRONMENT,
       '/api/infrastructure_provision/exploratory_environment');
     this.requestRegistry.Add(ApplicationServiceFacade.TEMPLATES,
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/services/index.ts b/services/self-service/src/main/resources/webapp/src/app/core/services/index.ts
index d84741540..68ce7e151 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/services/index.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/services/index.ts
@@ -40,3 +40,4 @@ export * from './storage.service';
 export * from './project.service';
 export * from './odahu-deployment.service';
 export * from './endpoint.service';
+export * from './user-images-page.service';
diff --git a/services/self-service/src/main/java/com/epam/datalab/backendapi/resources/dto/ImageInfoRecord.java b/services/self-service/src/main/resources/webapp/src/app/core/services/user-images-page.service.ts
similarity index 56%
copy from services/self-service/src/main/java/com/epam/datalab/backendapi/resources/dto/ImageInfoRecord.java
copy to services/self-service/src/main/resources/webapp/src/app/core/services/user-images-page.service.ts
index 18692c34c..05e9e9077 100644
--- a/services/self-service/src/main/java/com/epam/datalab/backendapi/resources/dto/ImageInfoRecord.java
+++ b/services/self-service/src/main/resources/webapp/src/app/core/services/user-images-page.service.ts
@@ -17,21 +17,23 @@
  * under the License.
  */
 
-package com.epam.datalab.backendapi.resources.dto;
+import {  Injectable } from '@angular/core';
+import {  Observable } from 'rxjs';
+import {  catchError } from 'rxjs/operators';
+import {  ErrorUtils } from '../util';
 
-import com.epam.datalab.dto.exploratory.ImageStatus;
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import lombok.Data;
+import { ApplicationServiceFacade } from './applicationServiceFacade.service';
+import { ProjectModel } from '../../resources/images';
 
-@Data
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class ImageInfoRecord {
-    private final String name;
-    private final String description;
-    private final String project;
-    private final String endpoint;
-    private final String user;
-    private final String application;
-    private final String fullName;
-    private final ImageStatus status;
+@Injectable()
+export class UserImagesPageService {
+  constructor(private applicationServiceFacade: ApplicationServiceFacade) { }
+
+
+  getUserImagePageInfo(): Observable<ProjectModel[]> {
+    return this.applicationServiceFacade.buildGetUserImagePage()
+      .pipe(
+        catchError(ErrorUtils.handleServiceError)
+      );
+  }
 }
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/services/userResource.service.ts b/services/self-service/src/main/resources/webapp/src/app/core/services/userResource.service.ts
index 3f2d5f8d6..05061c533 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/services/userResource.service.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/services/userResource.service.ts
@@ -23,6 +23,7 @@ import { catchError, map } from 'rxjs/operators';
 
 import { ErrorUtils } from '../util/';
 import { ApplicationServiceFacade } from './applicationServiceFacade.service';
+import {ProjectModel} from '../../resources/images/images.model';
 
 @Injectable()
 export class UserResourceService {
@@ -109,9 +110,9 @@ export class UserResourceService {
   }
 
   public suspendComputationalResource(
-    projectName: string, 
-    notebookName: string, 
-    computationalResourceName: string, 
+    projectName: string,
+    notebookName: string,
+    computationalResourceName: string,
     provider: string
   ): Observable<{}> {
     const body = JSON.stringify('/' + projectName + '/' + notebookName + '/' + computationalResourceName + '/terminate');
@@ -123,10 +124,10 @@ export class UserResourceService {
   }
 
   public toggleStopStartAction(
-    project: string, 
-    notebook: string, 
-    resource: string, 
-    action, 
+    project: string,
+    notebook: string,
+    resource: string,
+    action,
     provider: string
   ): Observable<{}> {
     const url = `/${project}/${notebook}/${resource}/${action}`;
diff --git a/services/self-service/src/main/resources/webapp/src/app/reports/reporting/reporting-grid/reporting-grid.component.ts b/services/self-service/src/main/resources/webapp/src/app/reports/reporting/reporting-grid/reporting-grid.component.ts
index 513ac45c1..9e45d9133 100644
--- a/services/self-service/src/main/resources/webapp/src/app/reports/reporting/reporting-grid/reporting-grid.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/reports/reporting/reporting-grid/reporting-grid.component.ts
@@ -62,13 +62,13 @@ export class ReportingGridComponent implements OnInit {
   isFiltered: boolean = false;
   active: object = {};
   displayedColumns: string[] = [
-    'name', 'user', 'project', 
-    'type', 'status', 'shape', 
+    'name', 'user', 'project',
+    'type', 'status', 'shape',
     'service', 'empty', 'charge'
   ];
   displayedFilterColumns: string[] = [
-    'name-filter', 'user-filter', 'project-filter', 
-    'type-filter', 'status-filter', 'shape-filter',  
+    'name-filter', 'user-filter', 'project-filter',
+    'type-filter', 'status-filter', 'shape-filter',
     'service-filter', 'empty-filter', 'actions'
   ];
   filtered: any;
@@ -106,7 +106,7 @@ export class ReportingGridComponent implements OnInit {
 
   ngOnInit() {
     this.userAgentIndex = window.navigator.userAgent.indexOf('Firefox');
-    
+
     window.setTimeout(() => {
       this.isScrollButtonsVisible = this.tableWrapper.nativeElement.offsetWidth - this.table._elementRef.nativeElement.offsetWidth < 0;
       this.checkMaxRight();
@@ -128,7 +128,6 @@ export class ReportingGridComponent implements OnInit {
 
   refreshData(fullReport, report) {
     this.reportData = [...report];
-    console.log(fullReport);
     this.fullReport = fullReport;
     this.checkFilters();
   }
@@ -168,7 +167,7 @@ export class ReportingGridComponent implements OnInit {
         return 0;
       });
     }
-    
+
     this.refreshData(this.fullReport, report);
     this.removeSorting();
     this.active[sortItem + direction] = true;
@@ -223,7 +222,7 @@ export class ReportingGridComponent implements OnInit {
     const arg = this.tableWrapper.nativeElement.offsetWidth +
       this.tableWrapper.nativeElement.scrollLeft + 2 <= this.table._elementRef.nativeElement.offsetWidth;
     return this.isMaxRight.next(arg);
-    
+
   }
 
   public onFilterNameUpdate(targetElement: any) {
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.ts
index 010e5d22e..05f822f4f 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.ts
@@ -127,9 +127,8 @@ export class BucketBrowserComponent implements OnInit, OnDestroy {
   }
 
   public toggleSelectedFile(file, type): void {
-    console.log(file, type);
-    type === 'file' 
-      ? file.isSelected = !file.isSelected 
+    type === 'file'
+      ? file.isSelected = !file.isSelected
       : file.isFolderSelected = !file.isFolderSelected;
     this.selected = this.folderItems.filter(item => item.isSelected);
     this.selectedFolderForAction = this.folderItems.filter(item => item.isFolderSelected);
@@ -260,8 +259,8 @@ export class BucketBrowserComponent implements OnInit, OnDestroy {
       this.objectPath = event.pathObject;
       this.path = event.path;
       this.originFolderItems = this.folderItems.map(v => v);
-      this.pathInsideBucket = this.path.indexOf('/') !== -1 
-        ? this.path.slice(this.path.indexOf('/') + 1) + '/' 
+      this.pathInsideBucket = this.path.indexOf('/') !== -1
+        ? this.path.slice(this.path.indexOf('/') + 1) + '/'
         : '';
       this.folderItems.forEach(item => item.isSelected = false);
     }
@@ -327,7 +326,7 @@ export class BucketBrowserComponent implements OnInit, OnDestroy {
       if (!file) {
         file = waitUploading[0];
       }
-      
+
       file.status = 'uploading';
       this.isFileUploading = this.addedFiles.some(v => v.status === 'uploading');
       this.isQueueFull = this.addedFiles.some(v => v.status === 'waiting');
@@ -350,7 +349,7 @@ export class BucketBrowserComponent implements OnInit, OnDestroy {
               this.sendFile(this.addedFiles.find(v => v.status === 'waiting'));
               this.bucketDataService.refreshBucketdata(this.bucketName, this.endpoint);
             }
-          }, 
+          },
           error => {
             window.clearInterval(file.interval);
             file.status = 'failed';
@@ -412,7 +411,7 @@ export class BucketBrowserComponent implements OnInit, OnDestroy {
                 selected[0].progress = 0;
               }, 1000);
             }
-          }, 
+          },
           error => {
             this.toastr.error(error.message || 'File downloading error!', 'Oops!');
             selected[0]['isDownloading'] = false;
@@ -461,7 +460,7 @@ export class BucketBrowserComponent implements OnInit, OnDestroy {
   public copyPath(): void {
     const selected = this.folderItems.filter(item => item.isSelected || item.isFolderSelected)[0];
     const pathToItem = `${this.pathInsideBucket}${selected.item}${selected.isFolderSelected ? '/' : ''}`;
-    
+
     const cloud = this.getCloud();
     const protocol = HelpUtils.getBucketProtocol(cloud);
     if (cloud !== 'azure') {
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.html
new file mode 100644
index 000000000..595cd2893
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.html
@@ -0,0 +1,240 @@
+<!--
+  ~ 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="image-list--wrapper">
+  <nav class="image-list__nav-bar">
+    <div class="selection">
+      <div class="mat-reset">
+        <div class="control selector-wrapper"
+             [ngClass]="{'disabled-select': !isProjectsMoreThanOne}"
+        >
+          <mat-form-field>
+            <mat-label>Select project</mat-label>
+
+            <mat-select
+              disableOptionCentering
+              panelClass="top-select scrolling"
+              [disabled]="!projectList.length"
+            >
+              <mat-option
+                *ngIf="isProjectsMoreThanOne"
+                (click)="onSelectClick('')"
+              >
+                Show all
+              </mat-option>
+              <mat-option
+                *ngFor="let project of projectList"
+                [value]="project"
+                (click)="onSelectClick(project)"
+              >
+                {{ project }}
+              </mat-option>
+              <mat-option *ngIf="!projectList?.length" class="multiple-select ml-10" disabled>
+                Projects list is empty
+              </mat-option>
+            </mat-select>
+            <button class="caret" [disabled]="false">
+              <i class="material-icons">keyboard_arrow_down</i>
+            </button>
+          </mat-form-field>
+        </div>
+      </div>
+    </div>
+
+    <div class="button--wrapper">
+      <span class="action-button--wrapper">
+          <button
+            type="button"
+            class="butt action-button"
+            mat-raised-button
+            [disabled]="true"
+            (click)="onActionClick()"
+          >
+            Actions
+            <i class="material-icons" >{{ !isActionsOpen ?  'expand_more' : 'expand_less' }}</i>
+          </button>
+          </span>
+      <span>
+        <button mat-raised-button class="butt">
+          <i class="material-icons highlight">autorenew</i>
+          Refresh
+        </button>
+      </span>
+    </div>
+  </nav>
+  <mat-divider></mat-divider>
+
+  <table mat-table [dataSource]="dataSource" class="mat-elevation-z8 demo-table data-grid">
+    <!-- Position Column -->
+    <ng-container matColumnDef="checkbox">
+      <th mat-header-cell *matHeaderCellDef class="image-checkbox--wrapper">
+        <div class="header-cell--wrapper">
+          <span>
+            <datalab-checkbox
+              (click)="allCheckboxToggle()"
+              [checked]="checkboxSelected"
+              class="image-checkbox"
+            ></datalab-checkbox>
+          </span>
+          <i class="material-icons header-cell__dots">
+            <span>more_vert</span>
+          </i>
+        </div>
+      </th>
+      <td mat-cell *matCellDef="let element" class="image-checkbox--wrapper">
+        <datalab-checkbox
+          (click)="onCheckboxClick(element)"
+          class="image-checkbox"
+          [checked]="element.isSelected"
+        ></datalab-checkbox>
+      </td>
+    </ng-container>
+
+    <ng-container matColumnDef="imageName">
+      <th mat-header-cell *matHeaderCellDef>
+        <div class="header-cell--wrapper">
+          <span>{{tableHeaderCellTitles.imageName}}</span>
+          <i class="material-icons header-cell__dots">
+            <span>more_vert</span>
+          </i>
+        </div>
+      </th>
+      <td mat-cell *matCellDef="let element"> {{element.name}} </td>
+    </ng-container>
+
+    <ng-container matColumnDef="creationDate">
+      <th mat-header-cell *matHeaderCellDef>
+        <div class="header-cell--wrapper">
+          <span>{{tableHeaderCellTitles.creationDate}}</span>
+          <i class="material-icons header-cell__dots">
+            <span>more_vert</span>
+          </i>
+        </div>
+      </th>
+      <td mat-cell *matCellDef="let element">
+        <span class="date-item"> {{element.creationDate | date: 'yyyy-MM-dd'}} </span>
+        <span> {{element.creationDate | date: 'HH:mm:ss'}} </span>
+      </td>
+    </ng-container>
+
+    <ng-container matColumnDef="provider">
+      <th mat-header-cell *matHeaderCellDef>
+        <div class="header-cell--wrapper">
+          <span>{{tableHeaderCellTitles.provider}}</span>
+          <i class="material-icons header-cell__dots">
+            <span>more_vert</span>
+          </i>
+        </div>
+      </th>
+      <td mat-cell *matCellDef="let element"> {{element.cloudProvider}} </td>
+    </ng-container>
+
+    <ng-container matColumnDef="imageStatus">
+      <th mat-header-cell *matHeaderCellDef>
+        <div class="header-cell--wrapper">
+          <span>{{tableHeaderCellTitles.imageStatus}}</span>
+          <i class="material-icons header-cell__dots">
+            <span>more_vert</span>
+          </i>
+        </div>
+      </th>
+      <td mat-cell *matCellDef="let element" ngClass="{{ element.status.toLowerCase() || ''}}">
+        {{element.status | capitalizeFirstLetter}}
+      </td>
+    </ng-container>
+
+    <ng-container matColumnDef="sharedStatus">
+      <th mat-header-cell *matHeaderCellDef>
+        <div class="header-cell--wrapper">
+          <span>{{tableHeaderCellTitles.sharedStatus}}</span>
+          <i class="material-icons header-cell__dots">
+            <span>more_vert</span>
+          </i>
+        </div>
+      </th>
+      <td mat-cell *matCellDef="let element">
+        <div class="shared-status--wrapper">
+          <span class="shared-status"> {{element.shared}} </span>
+          <span class="currency_details" >
+                <i class="material-icons">help_outline</i>
+              </span>
+        </div>
+      </td>
+    </ng-container>
+
+    <ng-container matColumnDef="templateName">
+      <th mat-header-cell *matHeaderCellDef>
+        <div class="header-cell--wrapper">
+          <span>{{tableHeaderCellTitles.templateName}}</span>
+          <i class="material-icons header-cell__dots">
+            <span>more_vert</span>
+          </i>
+        </div>
+      </th>
+      <td mat-cell *matCellDef="let element"> {{element.application}} </td>
+    </ng-container>
+
+    <ng-container matColumnDef="instanceName">
+      <th mat-header-cell *matHeaderCellDef>
+        <div class="header-cell--wrapper">
+          <span>{{tableHeaderCellTitles.instanceName}}</span>
+          <i class="material-icons header-cell__dots">
+            <span>more_vert</span>
+          </i>
+        </div>
+      </th>
+      <td mat-cell *matCellDef="let element"> {{element.instanceName}} </td>
+    </ng-container>
+
+    <ng-container matColumnDef="actions">
+      <th mat-header-cell *matHeaderCellDef> {{tableHeaderCellTitles.actions}} </th>
+      <td mat-cell *matCellDef="let element" class="settings actions-col">
+
+        <div class="button--wrapper">
+          <span class="currency_details" >
+                <i class="material-icons">help_outline</i>
+              </span>
+          <span #settings class="actions" (click)="actions.toggle($event, settings)"></span>
+        </div>
+        <bubble-up #actions class="list-menu" position="bottom-left" alternative="top-left">
+          <ul class="list-unstyled">
+            <li
+              matTooltip="Unable to terminate notebook because at least one compute is in progress"
+              matTooltipPosition="above"
+            >
+              <div>
+                <i class="material-icons">phonelink_off</i>
+                <span>Terminate</span>
+              </div>
+            </li>
+            <li>
+              <div>
+                <i class="material-icons">create</i>
+                <span>Share</span>
+              </div>
+            </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/images/images.component.scss b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.scss
new file mode 100644
index 000000000..e203266ef
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.scss
@@ -0,0 +1,102 @@
+/*!
+ * 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.
+ */
+
+.image-list {
+  &--wrapper {
+    padding: 12px 15px;
+  }
+
+  &__nav-bar {
+    display: flex;
+    justify-content: space-between;
+  }
+}
+
+.action-button {
+  padding: 0 38px 0 45px;
+
+  &--wrapper {
+    margin-right: 10px;
+  }
+}
+
+.demo-table {
+  width: 100%;
+}
+
+.mat-column-demo-position {
+  width: 32px;
+  border-right: 1px solid currentColor;
+  padding-right: 24px;
+  text-align: center;
+}
+
+.mat-column-demo-name {
+  padding-left: 16px;
+  font-size: 20px;
+}
+
+.mat-column-demo-weight {
+  font-style: italic;
+}
+
+.mat-column-demo-symbol {
+  width: 32px;
+  text-align: center;
+  font-weight: bold;
+}
+
+.image-checkbox {
+  &--wrapper {
+    padding-left: 10px !important;
+  }
+}
+
+.currency_details {
+  display: flex;
+  color: #35afd5;
+  cursor: pointer;
+  transition: all 0.45s ease-in-out;
+}
+
+.material-icons {
+  font-size: 18px;
+}
+
+.button--wrapper,
+.header-cell--wrapper {
+  display: flex;
+  justify-content: space-between;
+}
+
+.shared-status {
+  padding-right: 16px;
+
+  &--wrapper {
+    display: flex;
+  }
+}
+
+.header-cell__dots {
+  margin-right: 10px;
+}
+
+.date-item {
+  margin-right: 10px;
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.ts
new file mode 100644
index 000000000..cd39142fa
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.ts
@@ -0,0 +1,135 @@
+/*
+ * 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 { ToastrService } from 'ngx-toastr';
+
+import { GeneralEnvironmentStatus } from '../../administration/management/management.model';
+import { HealthStatusService, UserImagesPageService } from '../../core/services';
+import { ImageModel, ProjectModel } from './images.model';
+import { Image_Table_Column_Headers } from './images.config';
+
+
+interface Aaa {
+  isSelected?: boolean;
+  imageName: string;
+  creationDate: string;
+  provider: string;
+  imageStatus: string;
+  sharedStatus: 'Private' | 'Shared';
+  templateName: string;
+  instanceName: string;
+  actions: object;
+}
+
+
+const tableTitles = <const>['checkbox', 'imageName', 'creationDate', 'provider', 'imageStatus', 'sharedStatus', 'templateName', 'instanceName', 'actions'];
+
+@Component({
+  selector: 'datalab-images',
+  templateUrl: './images.component.html',
+  styleUrls: [
+    './images.component.scss',
+    '../resources-grid/resources-grid.component.scss',
+    '../resources.component.scss'
+  ]
+})
+
+export class ImagesComponent implements OnInit {
+  isActionsOpen: boolean = false;
+  healthStatus: GeneralEnvironmentStatus;
+  tableHeaderCellTitles: typeof Image_Table_Column_Headers = Image_Table_Column_Headers;
+  displayedColumns: typeof tableTitles = tableTitles;
+  dataSource: ImageModel[] = [];
+  checkboxSelected: boolean = false;
+  projectList: string[] = [];
+  private cashedImageListData: ProjectModel[] = [];
+
+  constructor(
+    private healthStatusService: HealthStatusService,
+    public toastr: ToastrService,
+    private userImagesPageService: UserImagesPageService
+  ) { }
+
+  ngOnInit(): void {
+    this.getEnvironmentHealthStatus();
+    this.getUserImagePageInfo();
+  }
+
+  onCheckboxClick(element: ImageModel) {
+    element.isSelected = !element.isSelected;
+  }
+
+  allCheckboxToggle(): void {
+    this.checkboxSelected = !this.checkboxSelected;
+
+    if (this.checkboxSelected) {
+      this.dataSource.forEach(image => image.isSelected = true);
+    } else {
+      this.dataSource.forEach(image => image.isSelected = false);
+    }
+  }
+
+  onActionClick(): void {
+    this.isActionsOpen = !this.isActionsOpen;
+  }
+
+  onSelectClick(projectName: string): void {
+    if (!projectName) {
+      this.dataSource = this.getImageList();
+    }
+    const { images } = this.cashedImageListData.find(({project}) => project === projectName);
+    this.dataSource = [...images];
+  }
+
+  private getImageList() {
+    return this.cashedImageListData.reduce((acc, {images}) => [...acc, ...images], []);
+  }
+
+  private getEnvironmentHealthStatus() {
+    this.healthStatusService.getEnvironmentHealthStatus().subscribe(
+      (result: GeneralEnvironmentStatus) => {
+        this.healthStatus = result;
+      },
+      error => this.toastr.error(error.message, 'Oops!')
+    );
+  }
+
+  private getUserImagePageInfo(): void {
+    this.userImagesPageService.getUserImagePageInfo().subscribe(imageListData => this.initImageTable(imageListData));
+  }
+
+  private initImageTable(imagePageList: ProjectModel[]) {
+    this.cashedImageListData = imagePageList;
+    this.getProjectList(imagePageList);
+    this.dataSource = this.getImageList();
+  }
+
+  private getProjectList(imagePageList: ProjectModel[]): void {
+    if (!imagePageList) {
+      return;
+    }
+    imagePageList.forEach(({project}) => this.projectList.push(project));
+  }
+
+  get isProjectsMoreThanOne () {
+    return this.projectList.length > 1;
+  }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.config.ts b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.config.ts
new file mode 100644
index 000000000..f2ac9f810
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.config.ts
@@ -0,0 +1,10 @@
+export enum Image_Table_Column_Headers {
+  imageName = 'Image name',
+  creationDate = 'Creation date',
+  provider = 'Provider',
+  imageStatus = 'Image status',
+  sharedStatus = 'Shared status',
+  templateName = 'Template name',
+  instanceName = 'Instance name',
+  actions = 'Actions',
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.model.ts b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.model.ts
new file mode 100644
index 000000000..b5b725a96
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.model.ts
@@ -0,0 +1,20 @@
+export interface ProjectModel {
+  project: string;
+  images: ImageModel[];
+}
+
+export interface ImageModel {
+  application: string;
+  cloudProvider: 'AWS' | 'GCP' | 'Azure';
+  creationDate: string;
+  description: string;
+  endpoint: string;
+  fullName: string;
+  instanceName: string;
+  name: string;
+  project: string;
+  shared: 'private' | 'shared';
+  status: 'created' | 'creating' | 'terminated' | 'terminating' | 'failed';
+  user: string;
+  isSelected?: boolean;
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/index.ts b/services/self-service/src/main/resources/webapp/src/app/resources/images/index.ts
new file mode 100644
index 000000000..365e9989d
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/index.ts
@@ -0,0 +1,2 @@
+export * from './images.config';
+export * from './images.model';
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 55c824d95..28b79ecd8 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
@@ -32,14 +32,13 @@ import { ProjectService, UserResourceService, OdahuDeploymentService } from '../
 import { ExploratoryModel } from './resources-grid.model';
 import { FilterConfigurationModel } from './filter-configuration.model';
 import { GeneralEnvironmentStatus } from '../../administration/management/management.model';
-import { ConfirmationDialogType } from '../../shared';
+import { ConfirmationDialogComponent, ConfirmationDialogType } from '../../shared';
 import { SortUtils, CheckUtils } from '../../core/util';
 import { DetailDialogComponent } from '../exploratory/detail-dialog';
 import { AmiCreateDialogComponent } from '../exploratory/ami-create-dialog';
 import { InstallLibrariesComponent } from '../exploratory/install-libraries';
 import { ComputationalResourceCreateDialogComponent } from '../computational/computational-resource-create-dialog/computational-resource-create-dialog.component';
 import { CostDetailsDialogComponent } from '../exploratory/cost-details-dialog';
-import { ConfirmationDialogComponent } from '../../shared/modal-dialog/confirmation-dialog';
 import { SchedulerComponent } from '../scheduler';
 import { DICTIONARY } from '../../../dictionary/global.dictionary';
 import { ProgressBarService } from '../../core/services/progress-bar.service';
@@ -195,9 +194,9 @@ export class ResourcesGridComponent implements OnInit {
     this.buildGrid();
   }
 
-  public containsNotebook(notebook_name: string, envoirmentNames: Array<string>): boolean {
-    if (notebook_name && envoirmentNames.length ) {
-      return envoirmentNames
+  public containsNotebook(notebook_name: string, environmentNames: Array<string>): boolean {
+    if (notebook_name && environmentNames.length ) {
+      return environmentNames
         .some(item => CheckUtils.delimitersFiltering(notebook_name) === CheckUtils.delimitersFiltering(item));
     }
       return false;
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.model.ts b/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.model.ts
index 8c8d8d7d8..64e5855a4 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.model.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.model.ts
@@ -148,10 +148,3 @@ export class ExploratoryModel {
     }
   }
 }
-
-// export interface Exploratory {
-//   project: string;
-//   endpoints: [];
-//   projectEndpoints: [];
-//   exploratory: ExploratoryModel[];
-// }
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/resources.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/resources.component.html
index 131aa638d..23117f7f2 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/resources.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/resources.component.html
@@ -26,9 +26,9 @@
         [matTooltipClass]="'full-size-tooltip'"
         [matTooltipDisabled]="healthStatus?.projectAssigned && resourcesGrid.activeProjectsList?.length !== 0"
       >
-        <button 
-          mat-raised-button 
-          class="butt butt-create" 
+        <button
+          mat-raised-button
+          class="butt butt-create"
           (click)="createEnvironment()"
           [disabled]="!healthStatus?.projectAssigned || !resourcesGrid.activeProjectsList?.length"
         >
@@ -36,26 +36,26 @@
         </button>
       </span>
       <div class="mat-reset">
-        <div class="control selector-wrapper" *ngIf="projects?.length" 
+        <div class="control selector-wrapper" *ngIf="projects?.length"
         [ngClass]="{'disabled-select': !isProjectsMoreThanOne}"
         >
           <mat-form-field>
             <mat-label>Select project</mat-label>
 
-            <mat-select 
+            <mat-select
               disableOptionCentering
-              [(value)]="resourcesGrid.activeProject" 
+              [(value)]="resourcesGrid.activeProject"
               panelClass="top-select scrolling"
               [disabled]="!isProjectsMoreThanOne"
             >
-              <mat-option 
-                *ngIf="projects?.length > 1" 
+              <mat-option
+                *ngIf="projects?.length > 1"
                 (click)="setActiveProject('')"
               >
                 Show all
               </mat-option>
-              <mat-option 
-                *ngFor="let project of projects" 
+              <mat-option
+                *ngFor="let project of projects"
                 [value]="project"
                 (click)="setActiveProject(project)"
               >
@@ -72,15 +72,15 @@
     </div>
 
     <div>
-      <span  
+      <span
         matTooltip="{{!this.bucketStatus?.view ? 'You have not permission to open bucket browser' : 'You have not any bucket'}}"
         matTooltipPosition="above"
         matTooltipDisabled="{{resourcesGrid.bucketsList?.length > 0 && this.bucketStatus?.view}}"
         [matTooltipClass]="'full-size-tooltip'"
       >
-        <button 
-          mat-raised-button 
-          class="butt butt-tool" 
+        <button
+          mat-raised-button
+          class="butt butt-tool"
           (click)="bucketBrowser(this.bucketStatus?.view)"
           [disabled]="!this.bucketStatus?.view || resourcesGrid.bucketsList?.length === 0"
         >
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/resources.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/resources.component.ts
index e876331bf..6b0a1b764 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/resources.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/resources.component.ts
@@ -86,7 +86,7 @@ export class ResourcesComponent implements OnInit {
           bucketStatus: this.bucketStatus,
           buckets: this.resourcesGrid.bucketsList
         },
-        panelClass: 'modal-fullscreen' 
+        panelClass: 'modal-fullscreen'
       })
       .afterClosed().subscribe();
   }
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/resources.module.ts b/services/self-service/src/main/resources/webapp/src/app/resources/resources.module.ts
index 47aea4c70..996c347d4 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/resources.module.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/resources.module.ts
@@ -31,6 +31,10 @@ import { MatTreeModule } from '@angular/material/tree';
 import { BucketDataService } from './bucket-browser/bucket-data.service';
 import { ConvertFileSizePipeModule } from '../core/pipes/convert-file-size';
 import { BucketBrowserModule } from './bucket-browser/bucket-browser.module';
+import { ImagesComponent } from './images/images.component';
+import {CheckboxModule} from '../shared/checkbox';
+import {BubbleModule} from '../shared';
+import { CapitalizeFirstLetterPipeModule } from '../core/pipes';
 
 @NgModule({
   imports: [
@@ -42,12 +46,16 @@ import { BucketBrowserModule } from './bucket-browser/bucket-browser.module';
     MaterialModule,
     MatTreeModule,
     ConvertFileSizePipeModule,
-    BucketBrowserModule
+    BucketBrowserModule,
+    CheckboxModule,
+    BubbleModule,
+    CapitalizeFirstLetterPipeModule
   ],
   declarations: [
     ResourcesComponent,
     ManageUngitComponent,
     ConfirmDeleteAccountDialogComponent,
+    ImagesComponent,
 
   ],
   entryComponents: [ManageUngitComponent, ConfirmDeleteAccountDialogComponent],
diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/bubble/bubble.component.ts b/services/self-service/src/main/resources/webapp/src/app/shared/bubble/bubble.component.ts
index 02e8ddef8..86bcfaaeb 100644
--- a/services/self-service/src/main/resources/webapp/src/app/shared/bubble/bubble.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/bubble/bubble.component.ts
@@ -17,17 +17,17 @@
  * under the License.
  */
 
-import { 
-  Component, 
-  Input, 
-  Output, 
-  EventEmitter, 
+import {
+  Component,
+  Input,
+  Output,
+  EventEmitter,
   HostBinding,
-  ChangeDetectorRef, 
-  ElementRef, 
+  ChangeDetectorRef,
+  ElementRef,
   OnDestroy,
-  ViewEncapsulation, 
-  HostListener 
+  ViewEncapsulation,
+  HostListener
 } from '@angular/core';
 import { BubblesCollector, BubbleService } from './bubble.service';
 
@@ -101,11 +101,14 @@ export class BubbleComponent implements OnDestroy {
           this.changeDirection = !this.isInViewport(bubbleElem);
 
           let isBubbleOutOfWrapper;
-          
-          if(document.querySelector('.wrapper')) {
-            isBubbleOutOfWrapper = bubbleElem.getBoundingClientRect().bottom > document.querySelector('.wrapper').getBoundingClientRect().bottom;
+
+          if (document.querySelector('.wrapper')) {
+            isBubbleOutOfWrapper = bubbleElem.getBoundingClientRect()
+              .bottom > document.querySelector('.wrapper')
+              .getBoundingClientRect()
+              .bottom;
           }
-          
+
           (this.changeDirection || isBubbleOutOfWrapper) && this.bubbleService.updatePosition(element, bubbleElem, this.alternative);
         }
 
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..60e635d99 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
@@ -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,70 @@
         <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]="['/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
+              class="sub-nav-item"
+              [style.margin-left.px]="isExpanded ? '30' : '0'"
+              [routerLink]="['/images']"
+              [routerLinkActive]="['active']"
+              [routerLinkActiveOptions]="{exact:true}"
+            >
+              <span *ngIf="isExpanded; else images">{{sideBarNames.images}}</span>
+              <ng-template #images><i class="material-icons">photo</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 +158,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 +183,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 +205,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 c88094060..4c906b3ef 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
@@ -42,7 +42,7 @@ import {
 } 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';
 
 interface Quota {
   projectQuotas: {};
@@ -93,7 +93,7 @@ export class NavbarComponent implements OnInit, OnDestroy {
   isExpanded: boolean = true;
   healthStatus: GeneralEnvironmentStatus;
   subscriptions: Subscription = new Subscription();
-  sideBarNames!: Record<string, string>;
+  sideBarNames: typeof Sidebar_Names_Config = Sidebar_Names_Config;
   userData!: UserInfo;
   commitMaxLength: number = 22;
 
@@ -109,7 +109,6 @@ export class NavbarComponent implements OnInit, OnDestroy {
   ) { }
 
   ngOnInit() {
-    this.sideBarNames = sideBarNamesConfig;
     this.applicationSecurityService.loggedInStatus.subscribe(response => {
       this.subscriptions.unsubscribe();
       this.subscriptions.closed = false;
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..44102f602 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,19 +17,20 @@
  * 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'
 }
 
 export interface UserInfo {
-    email: string;
-    name: string;
+  email: string;
+  name: string;
 }
diff --git a/services/self-service/src/main/resources/webapp/src/styles.scss b/services/self-service/src/main/resources/webapp/src/styles.scss
index 00a27bbe2..5bc005390 100644
--- a/services/self-service/src/main/resources/webapp/src/styles.scss
+++ b/services/self-service/src/main/resources/webapp/src/styles.scss
@@ -121,6 +121,7 @@ mat-chip.mat-chip strong {
   text-align: left;
 }
 
+.created,
 .running,
 .starting,
 .installed,
diff --git a/services/self-service/src/test/java/com/epam/datalab/backendapi/resources/ImageExploratoryResourceTest.java b/services/self-service/src/test/java/com/epam/datalab/backendapi/resources/ImageExploratoryResourceTest.java
index f62b1e3c6..6f7a0e224 100644
--- a/services/self-service/src/test/java/com/epam/datalab/backendapi/resources/ImageExploratoryResourceTest.java
+++ b/services/self-service/src/test/java/com/epam/datalab/backendapi/resources/ImageExploratoryResourceTest.java
@@ -24,6 +24,7 @@ import com.epam.datalab.backendapi.domain.RequestId;
 import com.epam.datalab.backendapi.resources.dto.ExploratoryImageCreateFormDTO;
 import com.epam.datalab.backendapi.resources.dto.ImageInfoRecord;
 import com.epam.datalab.backendapi.service.ImageExploratoryService;
+import com.epam.datalab.cloud.CloudProvider;
 import com.epam.datalab.dto.exploratory.ImageStatus;
 import com.epam.datalab.exceptions.ResourceAlreadyExistException;
 import com.epam.datalab.exceptions.ResourceNotFoundException;
@@ -39,6 +40,7 @@ import javax.ws.rs.core.GenericType;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
+import java.time.LocalDateTime;
 import java.util.Collections;
 import java.util.List;
 
@@ -275,8 +277,18 @@ public class ImageExploratoryResourceTest extends TestBase {
     }
 
     private List<ImageInfoRecord> getImageList() {
-        ImageInfoRecord imageInfoRecord = new ImageInfoRecord("someName", "someDescription", "someProject", "someEndpoint", "someUser", "someApp",
-                "someFullName", ImageStatus.CREATED);
+        ImageInfoRecord imageInfoRecord = new ImageInfoRecord("someName",
+                "2020-02-02",
+                "someDescription",
+                "someProject",
+                "someEndpoint",
+                "someUser",
+                "someApp",
+                "someInstance",
+                CloudProvider.AWS,
+                "someFullName",
+                ImageStatus.CREATED,
+                "private");
         return Collections.singletonList(imageInfoRecord);
     }
 }
diff --git a/services/self-service/src/test/java/com/epam/datalab/backendapi/service/impl/BillingServiceImplTest.java b/services/self-service/src/test/java/com/epam/datalab/backendapi/service/impl/BillingServiceImplTest.java
index 5c9ec461b..b2345a8c8 100644
--- a/services/self-service/src/test/java/com/epam/datalab/backendapi/service/impl/BillingServiceImplTest.java
+++ b/services/self-service/src/test/java/com/epam/datalab/backendapi/service/impl/BillingServiceImplTest.java
@@ -56,6 +56,7 @@ import org.mockito.runners.MockitoJUnitRunner;
 
 import javax.ws.rs.core.GenericType;
 import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
@@ -745,7 +746,19 @@ public class BillingServiceImplTest extends TestBase {
 
     private List<ImageInfoRecord> getImageInfoRecords() {
         return Collections.singletonList(
-                new ImageInfoRecord(IMAGE_NAME, IMAGE_DESCRIPTION, PROJECT, ENDPOINT, USER, IMAGE_APPLICATION, IMAGE_FULL_NAME, ImageStatus.CREATED)
+                new ImageInfoRecord(
+                        IMAGE_NAME,
+                        "2020-02-02",
+                        IMAGE_DESCRIPTION,
+                        PROJECT,
+                        ENDPOINT,
+                        USER,
+                        IMAGE_APPLICATION,
+                        EXPLORATORY_NAME,
+                        CloudProvider.GENERAL,
+                        IMAGE_FULL_NAME,
+                        ImageStatus.CREATED,
+                        "private")
         );
     }
 }
\ No newline at end of file
diff --git a/services/self-service/src/test/java/com/epam/datalab/backendapi/service/impl/ImageExploratoryServiceImplTest.java b/services/self-service/src/test/java/com/epam/datalab/backendapi/service/impl/ImageExploratoryServiceImplTest.java
index 78eb81f55..1af955aa8 100644
--- a/services/self-service/src/test/java/com/epam/datalab/backendapi/service/impl/ImageExploratoryServiceImplTest.java
+++ b/services/self-service/src/test/java/com/epam/datalab/backendapi/service/impl/ImageExploratoryServiceImplTest.java
@@ -52,6 +52,7 @@ import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.runners.MockitoJUnitRunner;
 
+import java.time.LocalDateTime;
 import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
@@ -308,8 +309,8 @@ public class ImageExploratoryServiceImplTest {
     }
 
     private ImageInfoRecord getImageInfoRecord() {
-        return new ImageInfoRecord("someName", "someDescription", "someProject", "someEndpoint", "someUser", "someApp",
-                "someFullName", ImageStatus.CREATED);
+        return new ImageInfoRecord("someName", "2020-02-02","someDescription", "someProject", "someEndpoint", "someUser", "someApp",
+                "someInstance",CloudProvider.GENERAL,"someFullName", ImageStatus.CREATED, "private");
     }
 
     private Image fetchImage() {


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


[incubator-datalab] 02/03: [feat/DATALAB-2811/view-list-of-all-images] added image page

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

hshpak pushed a commit to branch feat/DATALAB-2811/view-list-of-all-images
in repository https://gitbox.apache.org/repos/asf/incubator-datalab.git

commit 3289635e741fbe0aab37d927bd5b647ed828ce20
Merge: 7017c1f7c 659ffe439
Author: Hennadii_Shpak <bo...@gmail.com>
AuthorDate: Wed Jun 1 21:39:43 2022 +0300

    [feat/DATALAB-2811/view-list-of-all-images] added image page

 .../main/java/com/epam/datalab/model/exploratory/Image.java   |  2 ++
 .../datalab/backendapi/resources/dto/ImageInfoRecord.java     | 11 ++++++++++-
 .../backendapi/service/impl/ImageExploratoryServiceImpl.java  |  1 +
 .../webapp/src/app/resources/images/images.component.html     |  4 ++--
 .../resources/webapp/src/app/resources/images/images.model.ts |  2 +-
 5 files changed, 16 insertions(+), 4 deletions(-)

diff --cc services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.html
index 595cd2893,000000000..33a27ca87
mode 100644,000000..100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.html
@@@ -1,240 -1,0 +1,240 @@@
 +<!--
 +  ~ 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="image-list--wrapper">
 +  <nav class="image-list__nav-bar">
 +    <div class="selection">
 +      <div class="mat-reset">
 +        <div class="control selector-wrapper"
 +             [ngClass]="{'disabled-select': !isProjectsMoreThanOne}"
 +        >
 +          <mat-form-field>
 +            <mat-label>Select project</mat-label>
 +
 +            <mat-select
 +              disableOptionCentering
 +              panelClass="top-select scrolling"
 +              [disabled]="!projectList.length"
 +            >
 +              <mat-option
 +                *ngIf="isProjectsMoreThanOne"
 +                (click)="onSelectClick('')"
 +              >
 +                Show all
 +              </mat-option>
 +              <mat-option
 +                *ngFor="let project of projectList"
 +                [value]="project"
 +                (click)="onSelectClick(project)"
 +              >
 +                {{ project }}
 +              </mat-option>
 +              <mat-option *ngIf="!projectList?.length" class="multiple-select ml-10" disabled>
 +                Projects list is empty
 +              </mat-option>
 +            </mat-select>
 +            <button class="caret" [disabled]="false">
 +              <i class="material-icons">keyboard_arrow_down</i>
 +            </button>
 +          </mat-form-field>
 +        </div>
 +      </div>
 +    </div>
 +
 +    <div class="button--wrapper">
 +      <span class="action-button--wrapper">
 +          <button
 +            type="button"
 +            class="butt action-button"
 +            mat-raised-button
 +            [disabled]="true"
 +            (click)="onActionClick()"
 +          >
 +            Actions
 +            <i class="material-icons" >{{ !isActionsOpen ?  'expand_more' : 'expand_less' }}</i>
 +          </button>
 +          </span>
 +      <span>
 +        <button mat-raised-button class="butt">
 +          <i class="material-icons highlight">autorenew</i>
 +          Refresh
 +        </button>
 +      </span>
 +    </div>
 +  </nav>
 +  <mat-divider></mat-divider>
 +
 +  <table mat-table [dataSource]="dataSource" class="mat-elevation-z8 demo-table data-grid">
 +    <!-- Position Column -->
 +    <ng-container matColumnDef="checkbox">
 +      <th mat-header-cell *matHeaderCellDef class="image-checkbox--wrapper">
 +        <div class="header-cell--wrapper">
 +          <span>
 +            <datalab-checkbox
 +              (click)="allCheckboxToggle()"
 +              [checked]="checkboxSelected"
 +              class="image-checkbox"
 +            ></datalab-checkbox>
 +          </span>
 +          <i class="material-icons header-cell__dots">
 +            <span>more_vert</span>
 +          </i>
 +        </div>
 +      </th>
 +      <td mat-cell *matCellDef="let element" class="image-checkbox--wrapper">
 +        <datalab-checkbox
 +          (click)="onCheckboxClick(element)"
 +          class="image-checkbox"
 +          [checked]="element.isSelected"
 +        ></datalab-checkbox>
 +      </td>
 +    </ng-container>
 +
 +    <ng-container matColumnDef="imageName">
 +      <th mat-header-cell *matHeaderCellDef>
 +        <div class="header-cell--wrapper">
 +          <span>{{tableHeaderCellTitles.imageName}}</span>
 +          <i class="material-icons header-cell__dots">
 +            <span>more_vert</span>
 +          </i>
 +        </div>
 +      </th>
 +      <td mat-cell *matCellDef="let element"> {{element.name}} </td>
 +    </ng-container>
 +
 +    <ng-container matColumnDef="creationDate">
 +      <th mat-header-cell *matHeaderCellDef>
 +        <div class="header-cell--wrapper">
 +          <span>{{tableHeaderCellTitles.creationDate}}</span>
 +          <i class="material-icons header-cell__dots">
 +            <span>more_vert</span>
 +          </i>
 +        </div>
 +      </th>
 +      <td mat-cell *matCellDef="let element">
-         <span class="date-item"> {{element.creationDate | date: 'yyyy-MM-dd'}} </span>
-         <span> {{element.creationDate | date: 'HH:mm:ss'}} </span>
++        <span class="date-item"> {{element.timestamp | date: 'yyyy-MM-dd'}} </span>
++        <span> {{element.timestamp | date: 'HH:mm:ss'}} </span>
 +      </td>
 +    </ng-container>
 +
 +    <ng-container matColumnDef="provider">
 +      <th mat-header-cell *matHeaderCellDef>
 +        <div class="header-cell--wrapper">
 +          <span>{{tableHeaderCellTitles.provider}}</span>
 +          <i class="material-icons header-cell__dots">
 +            <span>more_vert</span>
 +          </i>
 +        </div>
 +      </th>
 +      <td mat-cell *matCellDef="let element"> {{element.cloudProvider}} </td>
 +    </ng-container>
 +
 +    <ng-container matColumnDef="imageStatus">
 +      <th mat-header-cell *matHeaderCellDef>
 +        <div class="header-cell--wrapper">
 +          <span>{{tableHeaderCellTitles.imageStatus}}</span>
 +          <i class="material-icons header-cell__dots">
 +            <span>more_vert</span>
 +          </i>
 +        </div>
 +      </th>
 +      <td mat-cell *matCellDef="let element" ngClass="{{ element.status.toLowerCase() || ''}}">
 +        {{element.status | capitalizeFirstLetter}}
 +      </td>
 +    </ng-container>
 +
 +    <ng-container matColumnDef="sharedStatus">
 +      <th mat-header-cell *matHeaderCellDef>
 +        <div class="header-cell--wrapper">
 +          <span>{{tableHeaderCellTitles.sharedStatus}}</span>
 +          <i class="material-icons header-cell__dots">
 +            <span>more_vert</span>
 +          </i>
 +        </div>
 +      </th>
 +      <td mat-cell *matCellDef="let element">
 +        <div class="shared-status--wrapper">
 +          <span class="shared-status"> {{element.shared}} </span>
 +          <span class="currency_details" >
 +                <i class="material-icons">help_outline</i>
 +              </span>
 +        </div>
 +      </td>
 +    </ng-container>
 +
 +    <ng-container matColumnDef="templateName">
 +      <th mat-header-cell *matHeaderCellDef>
 +        <div class="header-cell--wrapper">
 +          <span>{{tableHeaderCellTitles.templateName}}</span>
 +          <i class="material-icons header-cell__dots">
 +            <span>more_vert</span>
 +          </i>
 +        </div>
 +      </th>
 +      <td mat-cell *matCellDef="let element"> {{element.application}} </td>
 +    </ng-container>
 +
 +    <ng-container matColumnDef="instanceName">
 +      <th mat-header-cell *matHeaderCellDef>
 +        <div class="header-cell--wrapper">
 +          <span>{{tableHeaderCellTitles.instanceName}}</span>
 +          <i class="material-icons header-cell__dots">
 +            <span>more_vert</span>
 +          </i>
 +        </div>
 +      </th>
 +      <td mat-cell *matCellDef="let element"> {{element.instanceName}} </td>
 +    </ng-container>
 +
 +    <ng-container matColumnDef="actions">
 +      <th mat-header-cell *matHeaderCellDef> {{tableHeaderCellTitles.actions}} </th>
 +      <td mat-cell *matCellDef="let element" class="settings actions-col">
 +
 +        <div class="button--wrapper">
 +          <span class="currency_details" >
 +                <i class="material-icons">help_outline</i>
 +              </span>
 +          <span #settings class="actions" (click)="actions.toggle($event, settings)"></span>
 +        </div>
 +        <bubble-up #actions class="list-menu" position="bottom-left" alternative="top-left">
 +          <ul class="list-unstyled">
 +            <li
 +              matTooltip="Unable to terminate notebook because at least one compute is in progress"
 +              matTooltipPosition="above"
 +            >
 +              <div>
 +                <i class="material-icons">phonelink_off</i>
 +                <span>Terminate</span>
 +              </div>
 +            </li>
 +            <li>
 +              <div>
 +                <i class="material-icons">create</i>
 +                <span>Share</span>
 +              </div>
 +            </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 --cc services/self-service/src/main/resources/webapp/src/app/resources/images/images.model.ts
index b5b725a96,000000000..8e9ccae0c
mode 100644,000000..100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.model.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.model.ts
@@@ -1,20 -1,0 +1,20 @@@
 +export interface ProjectModel {
 +  project: string;
 +  images: ImageModel[];
 +}
 +
 +export interface ImageModel {
 +  application: string;
 +  cloudProvider: 'AWS' | 'GCP' | 'Azure';
-   creationDate: string;
++  timestamp: number;
 +  description: string;
 +  endpoint: string;
 +  fullName: string;
 +  instanceName: string;
 +  name: string;
 +  project: string;
 +  shared: 'private' | 'shared';
 +  status: 'created' | 'creating' | 'terminated' | 'terminating' | 'failed';
 +  user: string;
 +  isSelected?: boolean;
 +}


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


[incubator-datalab] 03/03: aaa

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

hshpak pushed a commit to branch feat/DATALAB-2811/view-list-of-all-images
in repository https://gitbox.apache.org/repos/asf/incubator-datalab.git

commit a2a18c6c5e9350a1f6d33d5ba5af51b3fe67d847
Author: Hennadii_Shpak <bo...@gmail.com>
AuthorDate: Thu Jun 2 12:56:40 2022 +0300

    aaa
---
 .../src/app/resources/images/images.component.html | 52 ++++++++++++++++------
 .../src/app/resources/images/images.component.scss | 37 +++++----------
 .../src/app/resources/images/images.component.ts   | 35 +++++----------
 .../src/app/resources/images/images.config.ts      | 36 +++++++++++++++
 .../src/app/resources/images/images.model.ts       |  2 +-
 5 files changed, 99 insertions(+), 63 deletions(-)

diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.html
index 33a27ca87..16f7bf728 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.html
@@ -63,13 +63,32 @@
             type="button"
             class="butt action-button"
             mat-raised-button
-            [disabled]="true"
+            [disabled]="isImageNotSelected"
             (click)="onActionClick()"
           >
             Actions
             <i class="material-icons" >{{ !isActionsOpen ?  'expand_more' : 'expand_less' }}</i>
           </button>
-          </span>
+        <div class="action-menu" *ngIf="isActionsOpen">
+          <button
+            type="button"
+            class="butt action-menu-item"
+            mat-raised-button
+            [disabled]="true"
+          >
+            Terminate
+          </button>
+
+          <button
+            type="button"
+            class="butt action-menu-item"
+            mat-raised-button
+            [disabled]="true"
+          >
+            Share
+          </button>
+        </div>
+      </span>
       <span>
         <button mat-raised-button class="butt">
           <i class="material-icons highlight">autorenew</i>
@@ -78,10 +97,10 @@
       </span>
     </div>
   </nav>
+
   <mat-divider></mat-divider>
 
-  <table mat-table [dataSource]="dataSource" class="mat-elevation-z8 demo-table data-grid">
-    <!-- Position Column -->
+  <table mat-table [dataSource]="dataSource" class="mat-elevation-z8 image-table data-grid">
     <ng-container matColumnDef="checkbox">
       <th mat-header-cell *matHeaderCellDef class="image-checkbox--wrapper">
         <div class="header-cell--wrapper">
@@ -92,7 +111,7 @@
               class="image-checkbox"
             ></datalab-checkbox>
           </span>
-          <i class="material-icons header-cell__dots">
+          <i class="material-icons">
             <span>more_vert</span>
           </i>
         </div>
@@ -110,7 +129,7 @@
       <th mat-header-cell *matHeaderCellDef>
         <div class="header-cell--wrapper">
           <span>{{tableHeaderCellTitles.imageName}}</span>
-          <i class="material-icons header-cell__dots">
+          <i class="material-icons">
             <span>more_vert</span>
           </i>
         </div>
@@ -122,7 +141,7 @@
       <th mat-header-cell *matHeaderCellDef>
         <div class="header-cell--wrapper">
           <span>{{tableHeaderCellTitles.creationDate}}</span>
-          <i class="material-icons header-cell__dots">
+          <i class="material-icons">
             <span>more_vert</span>
           </i>
         </div>
@@ -137,7 +156,7 @@
       <th mat-header-cell *matHeaderCellDef>
         <div class="header-cell--wrapper">
           <span>{{tableHeaderCellTitles.provider}}</span>
-          <i class="material-icons header-cell__dots">
+          <i class="material-icons">
             <span>more_vert</span>
           </i>
         </div>
@@ -149,7 +168,7 @@
       <th mat-header-cell *matHeaderCellDef>
         <div class="header-cell--wrapper">
           <span>{{tableHeaderCellTitles.imageStatus}}</span>
-          <i class="material-icons header-cell__dots">
+          <i class="material-icons">
             <span>more_vert</span>
           </i>
         </div>
@@ -163,14 +182,14 @@
       <th mat-header-cell *matHeaderCellDef>
         <div class="header-cell--wrapper">
           <span>{{tableHeaderCellTitles.sharedStatus}}</span>
-          <i class="material-icons header-cell__dots">
+          <i class="material-icons">
             <span>more_vert</span>
           </i>
         </div>
       </th>
       <td mat-cell *matCellDef="let element">
         <div class="shared-status--wrapper">
-          <span class="shared-status"> {{element.shared}} </span>
+          <span class="shared-status"> {{element.isShader ? sharedStatus.shared : sharedStatus.private}} </span>
           <span class="currency_details" >
                 <i class="material-icons">help_outline</i>
               </span>
@@ -182,7 +201,7 @@
       <th mat-header-cell *matHeaderCellDef>
         <div class="header-cell--wrapper">
           <span>{{tableHeaderCellTitles.templateName}}</span>
-          <i class="material-icons header-cell__dots">
+          <i class="material-icons">
             <span>more_vert</span>
           </i>
         </div>
@@ -194,7 +213,7 @@
       <th mat-header-cell *matHeaderCellDef>
         <div class="header-cell--wrapper">
           <span>{{tableHeaderCellTitles.instanceName}}</span>
-          <i class="material-icons header-cell__dots">
+          <i class="material-icons">
             <span>more_vert</span>
           </i>
         </div>
@@ -234,7 +253,14 @@
       </td>
     </ng-container>
 
+    <ng-container matColumnDef="placeholder">
+      <td mat-footer-cell *matFooterCellDef class="info" [colSpan]="displayedColumns.length - 1">
+        <span> There are no images yet </span>
+      </td>
+    </ng-container>
+
     <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
     <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
+    <tr [hidden]="dataSource.length" mat-footer-row *matFooterRowDef="['placeholder']"></tr>
   </table>
 </section>
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.scss b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.scss
index e203266ef..85487f421 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.scss
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.scss
@@ -32,36 +32,15 @@
   padding: 0 38px 0 45px;
 
   &--wrapper {
+    position: relative;
     margin-right: 10px;
   }
 }
 
-.demo-table {
+.image-table {
   width: 100%;
 }
 
-.mat-column-demo-position {
-  width: 32px;
-  border-right: 1px solid currentColor;
-  padding-right: 24px;
-  text-align: center;
-}
-
-.mat-column-demo-name {
-  padding-left: 16px;
-  font-size: 20px;
-}
-
-.mat-column-demo-weight {
-  font-style: italic;
-}
-
-.mat-column-demo-symbol {
-  width: 32px;
-  text-align: center;
-  font-weight: bold;
-}
-
 .image-checkbox {
   &--wrapper {
     padding-left: 10px !important;
@@ -93,10 +72,16 @@
   }
 }
 
-.header-cell__dots {
+.date-item {
   margin-right: 10px;
 }
 
-.date-item {
-  margin-right: 10px;
+.action-menu {
+  position: absolute;
+}
+
+.action-menu-item:disabled {
+  color: rgba(180, 179, 179, 0.87) !important;
+  background-color: white;
+  opacity: 1;
 }
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.ts
index cd39142fa..7e0e449d3 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.ts
@@ -24,23 +24,7 @@ import { ToastrService } from 'ngx-toastr';
 import { GeneralEnvironmentStatus } from '../../administration/management/management.model';
 import { HealthStatusService, UserImagesPageService } from '../../core/services';
 import { ImageModel, ProjectModel } from './images.model';
-import { Image_Table_Column_Headers } from './images.config';
-
-
-interface Aaa {
-  isSelected?: boolean;
-  imageName: string;
-  creationDate: string;
-  provider: string;
-  imageStatus: string;
-  sharedStatus: 'Private' | 'Shared';
-  templateName: string;
-  instanceName: string;
-  actions: object;
-}
-
-
-const tableTitles = <const>['checkbox', 'imageName', 'creationDate', 'provider', 'imageStatus', 'sharedStatus', 'templateName', 'instanceName', 'actions'];
+import { Image_Table_Column_Headers, Image_Table_Titles, Shared_Status } from './images.config';
 
 @Component({
   selector: 'datalab-images',
@@ -56,10 +40,11 @@ export class ImagesComponent implements OnInit {
   isActionsOpen: boolean = false;
   healthStatus: GeneralEnvironmentStatus;
   tableHeaderCellTitles: typeof Image_Table_Column_Headers = Image_Table_Column_Headers;
-  displayedColumns: typeof tableTitles = tableTitles;
+  displayedColumns: typeof Image_Table_Titles = Image_Table_Titles;
   dataSource: ImageModel[] = [];
   checkboxSelected: boolean = false;
   projectList: string[] = [];
+  readonly sharedStatus: typeof Shared_Status = Shared_Status;
   private cashedImageListData: ProjectModel[] = [];
 
   constructor(
@@ -73,7 +58,7 @@ export class ImagesComponent implements OnInit {
     this.getUserImagePageInfo();
   }
 
-  onCheckboxClick(element: ImageModel) {
+  onCheckboxClick(element: ImageModel): void {
     element.isSelected = !element.isSelected;
   }
 
@@ -99,11 +84,11 @@ export class ImagesComponent implements OnInit {
     this.dataSource = [...images];
   }
 
-  private getImageList() {
+  private getImageList(): ImageModel[] {
     return this.cashedImageListData.reduce((acc, {images}) => [...acc, ...images], []);
   }
 
-  private getEnvironmentHealthStatus() {
+  private getEnvironmentHealthStatus(): void {
     this.healthStatusService.getEnvironmentHealthStatus().subscribe(
       (result: GeneralEnvironmentStatus) => {
         this.healthStatus = result;
@@ -116,7 +101,7 @@ export class ImagesComponent implements OnInit {
     this.userImagesPageService.getUserImagePageInfo().subscribe(imageListData => this.initImageTable(imageListData));
   }
 
-  private initImageTable(imagePageList: ProjectModel[]) {
+  private initImageTable(imagePageList: ProjectModel[]): void {
     this.cashedImageListData = imagePageList;
     this.getProjectList(imagePageList);
     this.dataSource = this.getImageList();
@@ -129,7 +114,11 @@ export class ImagesComponent implements OnInit {
     imagePageList.forEach(({project}) => this.projectList.push(project));
   }
 
-  get isProjectsMoreThanOne () {
+  get isProjectsMoreThanOne(): boolean {
     return this.projectList.length > 1;
   }
+
+  get isImageNotSelected(): boolean {
+    return !this.dataSource.some(image => image.isSelected);
+  }
 }
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.config.ts b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.config.ts
index f2ac9f810..b57fbda4d 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.config.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.config.ts
@@ -1,3 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
 export enum Image_Table_Column_Headers {
   imageName = 'Image name',
   creationDate = 'Creation date',
@@ -8,3 +27,20 @@ export enum Image_Table_Column_Headers {
   instanceName = 'Instance name',
   actions = 'Actions',
 }
+
+export enum Shared_Status {
+  shared = 'Shared',
+  private = 'Private'
+}
+
+export const Image_Table_Titles = <const>[
+  'checkbox',
+  'imageName',
+  'creationDate',
+  'provider',
+  'imageStatus',
+  'sharedStatus',
+  'templateName',
+  'instanceName',
+  'actions'
+];
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.model.ts b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.model.ts
index 8e9ccae0c..daed68122 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.model.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.model.ts
@@ -13,7 +13,7 @@ export interface ImageModel {
   instanceName: string;
   name: string;
   project: string;
-  shared: 'private' | 'shared';
+  isShared: boolean;
   status: 'created' | 'creating' | 'terminated' | 'terminating' | 'failed';
   user: string;
   isSelected?: boolean;


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