You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dlab.apache.org by dg...@apache.org on 2020/06/10 09:10:27 UTC
[incubator-dlab] branch audit updated: [DLAB-1758]: Merge audit UI
(#784)
This is an automated email from the ASF dual-hosted git repository.
dgnatyshyn pushed a commit to branch audit
in repository https://gitbox.apache.org/repos/asf/incubator-dlab.git
The following commit(s) were added to refs/heads/audit by this push:
new e6d4e5d [DLAB-1758]: Merge audit UI (#784)
e6d4e5d is described below
commit e6d4e5dee441c32357d53fed0e47a4b52fba55dd
Author: Dmytro Gnatyshyn <42...@users.noreply.github.com>
AuthorDate: Wed Jun 10 12:09:32 2020 +0300
[DLAB-1758]: Merge audit UI (#784)
[DLAB-1758]: Merge audit UI
---
.../templates/proxy_location_webapp_template.conf | 2 +-
.../src/ssn/templates/ssn.yml | 2 +-
pom.xml | 1 +
...BucketDownloadDTO.java => BucketDeleteDTO.java} | 9 +-
services/provisioning-service/pom.xml | 22 +-
.../dlab/backendapi/resources/BucketResource.java | 74 ++-
.../dlab/backendapi/service/BucketService.java | 9 +-
.../service/impl/aws/BucketServiceAwsImpl.java | 51 +-
.../service/impl/azure/BucketServiceAzureImpl.java | 73 ++-
.../service/impl/gcp/BucketServiceGcpImpl.java | 37 +-
services/self-service/pom.xml | 10 +
services/self-service/self-service.yml | 2 +-
.../epam/dlab/backendapi/dao/ExploratoryDAO.java | 4 +-
.../epam/dlab/backendapi/dao/UserRoleDaoImpl.java | 20 +-
.../dlab/backendapi/resources/BucketResource.java | 82 ++-
.../backendapi/resources/ExploratoryResource.java | 3 -
.../azure/ComputationalResourceAzure.java | 3 -
.../backendapi/resources/dto/BucketDeleteDTO.java} | 11 +-
.../backendapi/resources/dto/UserResourceInfo.java | 70 +--
.../dlab/backendapi/resources/dto/UserRoleDto.java | 8 +
.../dlab/backendapi/service/BucketService.java | 7 +-
.../backendapi/service/impl/BucketServiceImpl.java | 43 +-
.../service/impl/EndpointServiceImpl.java | 6 +-
.../service/impl/EnvironmentServiceImpl.java | 29 +-
.../impl/InfrastructureInfoServiceImpl.java | 60 +--
.../impl/InfrastructureTemplateServiceImpl.java | 35 +-
.../src/main/resources/mongo/aws/mongo_roles.json | 8 +-
.../main/resources/mongo/azure/mongo_roles.json | 8 +-
.../src/main/resources/mongo/gcp/mongo_roles.json | 8 +-
.../manage-environment-dilog.component.html | 2 +-
.../manage-environment-dilog.component.ts | 4 +
.../management-grid/management-grid.component.html | 5 +-
.../management-grid/management-grid.component.scss | 5 +-
.../management-grid/management-grid.component.ts | 31 +-
.../administration/management/management.model.ts | 6 +-
.../main/resources/webapp/src/app/app.module.ts | 10 +-
.../resources/webapp/src/app/app.routing.module.ts | 12 +-
.../convert-file-size/convert-file-size.pipe.ts} | 36 +-
.../src/app/core/pipes/convert-file-size/index.ts} | 21 +-
.../src/app/core/services/appRouting.service.ts | 1 -
.../services/applicationServiceFacade.service.ts | 24 +-
.../webapp/src/app/core/services/audit.service.ts | 44 ++
.../app/core/services/bucket-browser.service.ts | 6 +-
.../webapp/src/app/core/util/copyPathUtils.ts} | 27 +-
.../resources/webapp/src/app/core/util/patterns.ts | 1 +
.../webapp/src/app/core/util/sortUtils.ts | 8 +-
.../audit/audit-grid/audit-grid.component.html | 147 ++++++
.../audit/audit-grid/audit-grid.component.scss | 237 +++++++++
.../audit/audit-grid/audit-grid.component.ts | 135 +++++
.../audit-toolbar/audit-toolbar.component.html} | 10 +-
.../audit-toolbar/audit-toolbar.component.scss} | 0
.../audit-toolbar/audit-toolbar.component.ts} | 34 +-
.../src/app/reports/audit/audit.component.ts | 99 ++++
.../audit/audit.module.ts} | 26 +-
.../src/app/reports/audit/filter-audit.model.ts | 25 +
.../reporting-grid/reporting-grid.component.html | 2 +-
.../reporting-grid/reporting-grid.component.scss | 0
.../reporting-grid/reporting-grid.component.ts | 12 +-
.../{ => reports}/reporting/reporting.component.ts | 9 +-
.../{ => reports}/reporting/reporting.module.ts | 6 +-
.../reporting/toolbar/toolbar.component.html | 8 +-
.../reporting/toolbar/toolbar.component.scss | 0
.../reporting/toolbar/toolbar.component.ts | 10 +-
.../webapp/src/app/reports/reports.module.ts} | 22 +-
.../bucket-browser/bucket-browser.component.html | 285 ++++++++---
.../bucket-browser/bucket-browser.component.scss | 556 +++++++++++++++++++--
.../bucket-browser/bucket-browser.component.ts | 395 ++++++++++++---
.../bucket-browser.module.ts} | 35 +-
.../bucket-confirmation-dialog.component.html | 122 +++++
.../bucket-confirmation-dialog.component.scss | 176 +++++++
.../bucket-confirmation-dialog.component.ts | 58 +++
.../bucket-browser/bucket-data.service.ts | 87 +++-
.../buckets-tree/bucket-tree.component.html | 25 +
.../buckets-tree/bucket-tree.component.scss | 106 ++++
.../buckets-tree/bucket-tree.component.ts | 101 ++++
.../folder-tree/folder-tree.component.html | 75 ++-
.../folder-tree/folder-tree.component.scss | 69 ++-
.../folder-tree/folder-tree.component.ts | 203 ++++----
.../cluster-details/cluster-details.component.html | 2 +-
.../cluster-details/cluster-details.component.ts | 11 +-
.../cost-details-dialog.component.html | 4 +-
.../detail-dialog/detail-dialog.component.html | 154 ++++--
.../detail-dialog/detail-dialog.component.scss | 19 +-
.../detail-dialog/detail-dialog.component.ts | 40 +-
.../install-libraries.component.ts | 10 +-
.../resources-grid/resources-grid.component.ts | 40 +-
.../resources-grid/resources-grid.model.ts | 4 +
.../src/app/resources/resources.component.html | 24 +-
.../src/app/resources/resources.component.ts | 18 +-
.../webapp/src/app/resources/resources.module.ts | 30 +-
.../multi-level-select-dropdown.component.ts | 2 +-
.../webapp/src/app/shared/material.module.ts | 3 +-
.../src/app/shared/navbar/navbar.component.html | 50 +-
.../src/assets/img/blank-file-svgrepo-com.svg | 41 ++
.../webapp/src/assets/styles/_dialogs.scss | 62 +++
.../src/main/resources/webapp/src/styles.scss | 14 +-
96 files changed, 3688 insertions(+), 855 deletions(-)
diff --git a/infrastructure-provisioning/src/ssn/templates/proxy_location_webapp_template.conf b/infrastructure-provisioning/src/ssn/templates/proxy_location_webapp_template.conf
index 2b949e0..38958b4 100644
--- a/infrastructure-provisioning/src/ssn/templates/proxy_location_webapp_template.conf
+++ b/infrastructure-provisioning/src/ssn/templates/proxy_location_webapp_template.conf
@@ -28,5 +28,5 @@ location / {
# Fix the "It appears that your reverse proxy set up is broken" error.
proxy_pass https://localhost:8443;
- proxy_read_timeout 90;
+ proxy_read_timeout 1800;
}
\ No newline at end of file
diff --git a/infrastructure-provisioning/src/ssn/templates/ssn.yml b/infrastructure-provisioning/src/ssn/templates/ssn.yml
index 41a9723..9ccd8d8 100644
--- a/infrastructure-provisioning/src/ssn/templates/ssn.yml
+++ b/infrastructure-provisioning/src/ssn/templates/ssn.yml
@@ -64,7 +64,7 @@ provisioningService:
bucketService:
jerseyClient:
- timeout: 10m
+ timeout: 50m
connectionTimeout: 3s
billingService:
diff --git a/pom.xml b/pom.xml
index b6808a8..e24bfd9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -86,6 +86,7 @@
<lombok.version>1.16.18</lombok.version>
<hibernate.validator.version>5.4.2.Final</hibernate.validator.version>
<logback.version>1.2.3</logback.version>
+ <commons-fileupload.version>1.3.3</commons-fileupload.version>
</properties>
<dependencyManagement>
<dependencies>
diff --git a/services/dlab-model/src/main/java/com/epam/dlab/dto/bucket/BucketDownloadDTO.java b/services/dlab-model/src/main/java/com/epam/dlab/dto/bucket/BucketDeleteDTO.java
similarity index 82%
copy from services/dlab-model/src/main/java/com/epam/dlab/dto/bucket/BucketDownloadDTO.java
copy to services/dlab-model/src/main/java/com/epam/dlab/dto/bucket/BucketDeleteDTO.java
index b1201e6..0bec33d 100644
--- a/services/dlab-model/src/main/java/com/epam/dlab/dto/bucket/BucketDownloadDTO.java
+++ b/services/dlab-model/src/main/java/com/epam/dlab/dto/bucket/BucketDeleteDTO.java
@@ -21,13 +21,12 @@ package com.epam.dlab.dto.bucket;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
-import org.hibernate.validator.constraints.NotBlank;
+
+import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
-public class BucketDownloadDTO {
- @NotBlank(message = "field cannot be empty")
+public class BucketDeleteDTO {
private final String bucket;
- @NotBlank(message = "field cannot be empty")
- private final String object;
+ private final List<String> objects;
}
diff --git a/services/provisioning-service/pom.xml b/services/provisioning-service/pom.xml
index 2fc22bd..0cec6a5 100644
--- a/services/provisioning-service/pom.xml
+++ b/services/provisioning-service/pom.xml
@@ -34,14 +34,14 @@
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>libraries-bom</artifactId>
- <version>3.3.0</version>
+ <version>5.3.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bom</artifactId>
- <version>2.11.9</version>
+ <version>2.13.9</version>
<type>pom</type>
<scope>import</scope>
</dependency>
@@ -90,7 +90,7 @@
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-storage</artifactId>
- <version>1.106.0</version>
+ <version>1.107.0</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
@@ -101,13 +101,27 @@
<artifactId>httpclient</artifactId>
<version>4.5.9</version>
</dependency>
-
+ <dependency>
+ <groupId>com.azure</groupId>
+ <artifactId>azure-storage-blob</artifactId>
+ <version>12.6.0</version>
+ </dependency>
+ <dependency>
+ <groupId>com.fasterxml.jackson.core</groupId>
+ <artifactId>jackson-core</artifactId>
+ <version>2.11.0</version>
+ </dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
<dependency>
+ <groupId>commons-fileupload</groupId>
+ <artifactId>commons-fileupload</artifactId>
+ <version>${commons-fileupload.version}</version>
+ </dependency>
+ <dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${org.mockito.version}</version>
diff --git a/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/resources/BucketResource.java b/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/resources/BucketResource.java
index a44d21f..4495f5d 100644
--- a/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/resources/BucketResource.java
+++ b/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/resources/BucketResource.java
@@ -21,19 +21,25 @@ package com.epam.dlab.backendapi.resources;
import com.epam.dlab.auth.UserInfo;
import com.epam.dlab.backendapi.service.BucketService;
+import com.epam.dlab.dto.bucket.BucketDeleteDTO;
+import com.epam.dlab.exceptions.DlabException;
import com.google.inject.Inject;
import io.dropwizard.auth.Auth;
import lombok.extern.slf4j.Slf4j;
-import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
-import org.glassfish.jersey.media.multipart.FormDataParam;
+import org.apache.commons.fileupload.FileItemIterator;
+import org.apache.commons.fileupload.FileItemStream;
+import org.apache.commons.fileupload.servlet.ServletFileUpload;
+import org.apache.commons.fileupload.util.Streams;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.InputStream;
@@ -41,6 +47,10 @@ import java.io.InputStream;
@Slf4j
@Path("/bucket")
public class BucketResource {
+ private static final String OBJECT_FORM_FIELD = "object";
+ private static final String BUCKET_FORM_FIELD = "bucket";
+ private static final String SIZE_FORM_FIELD = "file-size";
+
private final BucketService bucketService;
@Inject
@@ -61,12 +71,8 @@ public class BucketResource {
@Path("/upload")
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
- public Response uploadObject(@Auth UserInfo userInfo,
- @FormDataParam("object") String object,
- @FormDataParam("bucket") String bucket,
- @FormDataParam("file") InputStream inputStream,
- @FormDataParam("file") FormDataContentDisposition fileMetaData) {
- bucketService.uploadObject(bucket, object, inputStream);
+ public Response uploadObject(@Auth UserInfo userInfo, @Context HttpServletRequest request) {
+ upload(request);
return Response.ok().build();
}
@@ -74,20 +80,54 @@ public class BucketResource {
@Path("/{bucket}/object/{object}/download")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_OCTET_STREAM)
- public Response downloadObject(@Auth UserInfo userInfo,
+ public Response downloadObject(@Auth UserInfo userInfo, @Context HttpServletResponse resp,
@PathParam("object") String object,
@PathParam("bucket") String bucket) {
- return Response.ok(bucketService.downloadObject(bucket, object)).build();
+ bucketService.downloadObject(bucket, object, resp);
+ return Response.ok().build();
}
- @DELETE
- @Path("/{bucket}/object/{object}")
+ @POST
+ @Path("/objects/delete")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
- public Response uploadObject(@Auth UserInfo userInfo,
- @PathParam("bucket") String bucket,
- @PathParam("object") String object) {
- bucketService.deleteObject(bucket, object);
+ public Response uploadObject(@Auth UserInfo userInfo, BucketDeleteDTO bucketDeleteDTO) {
+ bucketService.deleteObjects(bucketDeleteDTO.getBucket(), bucketDeleteDTO.getObjects());
return Response.ok().build();
}
+
+ private void upload(HttpServletRequest request) {
+ String object = null;
+ String bucket = null;
+ long fileSize = 0;
+
+ ServletFileUpload upload = new ServletFileUpload();
+ try {
+ FileItemIterator iterStream = upload.getItemIterator(request);
+ while (iterStream.hasNext()) {
+ FileItemStream item = iterStream.next();
+ try (InputStream stream = item.openStream()) {
+ if (item.isFormField()) {
+ if (OBJECT_FORM_FIELD.equals(item.getFieldName())) {
+ object = Streams.asString(stream);
+ }
+ if (BUCKET_FORM_FIELD.equals(item.getFieldName())) {
+ bucket = Streams.asString(stream);
+ }
+ if (SIZE_FORM_FIELD.equals(item.getFieldName())) {
+ fileSize = Long.parseLong(Streams.asString(stream));
+ }
+ } else {
+ bucketService.uploadObject(bucket, object, stream, fileSize);
+ }
+ } catch (Exception e) {
+ log.error("Cannot upload object {} to bucket {}. {}", object, bucket, e.getMessage(), e);
+ throw new DlabException(String.format("Cannot upload object %s to bucket %s. %s", object, bucket, e.getMessage()));
+ }
+ }
+ } catch (Exception e) {
+ log.error("Cannot upload object {} to bucket {}. {}", object, bucket, e.getMessage(), e);
+ throw new DlabException(String.format("Cannot upload object %s to bucket %s. %s", object, bucket, e.getMessage()));
+ }
+ }
}
diff --git a/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/service/BucketService.java b/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/service/BucketService.java
index f3ed208..c1488e3 100644
--- a/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/service/BucketService.java
+++ b/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/service/BucketService.java
@@ -21,17 +21,18 @@ package com.epam.dlab.backendapi.service;
import com.epam.dlab.dto.bucket.BucketDTO;
+import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.util.List;
public interface BucketService {
- String DATE_FORMAT = "dd-M-yyyy hh:mm:ss";
+ String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss";
List<BucketDTO> getObjects(String bucket);
- void uploadObject(String bucket, String object, InputStream stream);
+ void uploadObject(String bucket, String object, InputStream stream, long fileSize);
- byte[] downloadObject(String bucket, String object);
+ void downloadObject(String bucket, String object, HttpServletResponse resp);
- void deleteObject(String bucket, String object);
+ void deleteObjects(String bucket, List<String> objects);
}
diff --git a/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/service/impl/aws/BucketServiceAwsImpl.java b/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/service/impl/aws/BucketServiceAwsImpl.java
index 8193fde..347543c 100644
--- a/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/service/impl/aws/BucketServiceAwsImpl.java
+++ b/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/service/impl/aws/BucketServiceAwsImpl.java
@@ -23,18 +23,19 @@ import com.epam.dlab.backendapi.service.BucketService;
import com.epam.dlab.dto.bucket.BucketDTO;
import com.epam.dlab.exceptions.DlabException;
import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.io.IOUtils;
-import software.amazon.awssdk.awscore.exception.AwsServiceException;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.core.sync.ResponseTransformer;
import software.amazon.awssdk.services.s3.S3Client;
-import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
+import software.amazon.awssdk.services.s3.model.Delete;
+import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.ListObjectsRequest;
+import software.amazon.awssdk.services.s3.model.ObjectIdentifier;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.S3Object;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
@@ -64,7 +65,8 @@ public class BucketServiceAwsImpl implements BucketService {
}
@Override
- public void uploadObject(String bucket, String object, InputStream stream) {
+ public void uploadObject(String bucket, String object, InputStream stream, long fileSize) {
+ log.info("Uploading file {} to bucket {}", object, bucket);
try {
S3Client s3 = S3Client.create();
PutObjectRequest uploadRequest = PutObjectRequest
@@ -72,53 +74,64 @@ public class BucketServiceAwsImpl implements BucketService {
.bucket(bucket)
.key(object)
.build();
- s3.putObject(uploadRequest, RequestBody.fromBytes(IOUtils.toByteArray(stream)));
+ s3.putObject(uploadRequest, RequestBody.fromInputStream(stream, fileSize));
} catch (Exception e) {
log.error("Cannot upload object {} to bucket {}. Reason: {}", object, bucket, e.getMessage());
throw new DlabException(String.format("Cannot upload object %s to bucket %s. Reason: %s", object, bucket, e.getMessage()));
}
+ log.info("Finished uploading file {} to bucket {}", object, bucket);
}
@Override
- public byte[] downloadObject(String bucket, String object) {
- try {
+ public void downloadObject(String bucket, String object, HttpServletResponse resp) {
+ log.info("Downloading file {} from bucket {}", object, bucket);
+ try (ServletOutputStream outputStream = resp.getOutputStream()) {
S3Client s3 = S3Client.create();
GetObjectRequest downloadRequest = GetObjectRequest
.builder()
.bucket(bucket)
.key(object)
.build();
- return s3.getObject(downloadRequest, ResponseTransformer.toBytes()).asByteArray();
+ s3.getObject(downloadRequest, ResponseTransformer.toOutputStream(outputStream));
} catch (Exception e) {
log.error("Cannot download object {} from bucket {}. Reason: {}", object, bucket, e.getMessage());
throw new DlabException(String.format("Cannot download object %s from bucket %s. Reason: %s", object, bucket, e.getMessage()));
}
+ log.info("Finished downloading file {} from bucket {}", object, bucket);
}
@Override
- public void deleteObject(String bucket, String object) {
+ public void deleteObjects(String bucket, List<String> objects) {
try {
S3Client s3 = S3Client.create();
- DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest
- .builder()
+ List<ObjectIdentifier> objectsToDelete = objects
+ .stream()
+ .map(o -> ObjectIdentifier.builder()
+ .key(o)
+ .build())
+ .collect(Collectors.toList());
+
+ DeleteObjectsRequest deleteObjectsRequests = DeleteObjectsRequest.builder()
.bucket(bucket)
- .key(object)
+ .delete(Delete.builder()
+ .objects(objectsToDelete)
+ .build())
.build();
- s3.deleteObject(deleteObjectRequest);
- } catch (AwsServiceException e) {
- log.error("Cannot delete object {} from bucket {}. Reason: {}", object, bucket, e.getMessage());
- throw new DlabException(String.format("Cannot delete object %s from bucket %s. Reason: %s", object, bucket, e.getMessage()));
+
+ s3.deleteObjects(deleteObjectsRequests);
+ } catch (Exception e) {
+ log.error("Cannot delete objects {} from bucket {}. Reason: {}", objects, bucket, e.getMessage());
+ throw new DlabException(String.format("Cannot delete objects %s from bucket %s. Reason: %s", objects, bucket, e.getMessage()));
}
}
private BucketDTO toBucketDTO(String bucket, S3Object s3Object) {
- final String size = FileUtils.byteCountToDisplaySize(s3Object.size());
Date date = Date.from(s3Object.lastModified());
SimpleDateFormat formatter = new SimpleDateFormat(DATE_FORMAT);
return BucketDTO.builder()
.bucket(bucket)
.object(s3Object.key())
- .size(size)
+ .size(String.valueOf(s3Object.size()))
.lastModifiedDate(formatter.format(date))
.build();
}
diff --git a/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/service/impl/azure/BucketServiceAzureImpl.java b/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/service/impl/azure/BucketServiceAzureImpl.java
index 0799b8e..f35dbc6 100644
--- a/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/service/impl/azure/BucketServiceAzureImpl.java
+++ b/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/service/impl/azure/BucketServiceAzureImpl.java
@@ -19,30 +19,91 @@
package com.epam.dlab.backendapi.service.impl.azure;
+import com.azure.storage.blob.BlobClient;
+import com.azure.storage.blob.BlobContainerClient;
+import com.azure.storage.blob.BlobServiceClient;
+import com.azure.storage.blob.BlobServiceClientBuilder;
+import com.azure.storage.blob.models.BlobItem;
import com.epam.dlab.backendapi.service.BucketService;
import com.epam.dlab.dto.bucket.BucketDTO;
+import com.epam.dlab.exceptions.DlabException;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.FileUtils;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
+import java.time.format.DateTimeFormatter;
import java.util.List;
+import java.util.stream.Collectors;
+@Slf4j
public class BucketServiceAzureImpl implements BucketService {
@Override
public List<BucketDTO> getObjects(String bucket) {
- return null;
+ try {
+ BlobServiceClient blobServiceClient = new BlobServiceClientBuilder().connectionString(System.getenv("AZURE_STORAGE_CONNECTION_STRING")).buildClient();
+ BlobContainerClient blobContainerClient = blobServiceClient.getBlobContainerClient(bucket);
+ return blobContainerClient.listBlobs()
+ .stream()
+ .map(blob -> toBucketDTO(bucket, blob))
+ .collect(Collectors.toList());
+ } catch (Exception e) {
+ log.error("Cannot retrieve objects from bucket {}. Reason: {}", bucket, e.getMessage());
+ throw new DlabException(String.format("Cannot retrieve objects from bucket %s. Reason: %s", bucket, e.getMessage()));
+ }
}
@Override
- public void uploadObject(String bucket, String object, InputStream stream) {
-
+ public void uploadObject(String bucket, String object, InputStream stream, long fileSize) {
+ log.info("Uploading file {} to bucket {}", object, bucket);
+ try {
+ BlobServiceClient blobServiceClient = new BlobServiceClientBuilder().connectionString(System.getenv("AZURE_STORAGE_CONNECTION_STRING")).buildClient();
+ BlobContainerClient blobContainerClient = blobServiceClient.getBlobContainerClient(bucket);
+ BlobClient blobClient = blobContainerClient.getBlobClient(object);
+ blobClient.upload(stream, fileSize);
+ } catch (Exception e) {
+ log.error("Cannot upload object {} to bucket {}. Reason: {}", object, bucket, e.getMessage());
+ throw new DlabException(String.format("Cannot upload object %s to bucket %s. Reason: %s", object, bucket, e.getMessage()));
+ }
+ log.info("Finished uploading file {} to bucket {}", object, bucket);
}
@Override
- public byte[] downloadObject(String bucket, String object) {
- return new byte[0];
+ public void downloadObject(String bucket, String object, HttpServletResponse resp) {
+ log.info("Downloading file {} from bucket {}", object, bucket);
+ try (ServletOutputStream outputStream = resp.getOutputStream()) {
+ BlobServiceClient blobServiceClient = new BlobServiceClientBuilder().connectionString(System.getenv("AZURE_STORAGE_CONNECTION_STRING")).buildClient();
+ BlobContainerClient blobContainerClient = blobServiceClient.getBlobContainerClient(bucket);
+ BlobClient blobClient = blobContainerClient.getBlobClient(object);
+ blobClient.download(outputStream);
+ } catch (Exception e) {
+ log.error("Cannot download object {} from bucket {}. Reason: {}", object, bucket, e.getMessage());
+ throw new DlabException(String.format("Cannot download object %s from bucket %s. Reason: %s", object, bucket, e.getMessage()));
+ }
+ log.info("Finished downloading file {} from bucket {}", object, bucket);
}
@Override
- public void deleteObject(String bucket, String object) {
+ public void deleteObjects(String bucket, List<String> objects) {
+ try {
+ BlobServiceClient blobServiceClient = new BlobServiceClientBuilder().connectionString(System.getenv("AZURE_STORAGE_CONNECTION_STRING")).buildClient();
+ BlobContainerClient blobContainerClient = blobServiceClient.getBlobContainerClient(bucket);
+ objects.forEach(object -> blobContainerClient.getBlobClient(object).delete());
+ } catch (Exception e) {
+ log.error("Cannot delete objects {} from bucket {}. Reason: {}", objects, bucket, e.getMessage());
+ throw new DlabException(String.format("Cannot delete objects %s from bucket %s. Reason: %s", objects, bucket, e.getMessage()));
+ }
+ }
+ private BucketDTO toBucketDTO(String bucket, BlobItem blob) {
+ final String size = FileUtils.byteCountToDisplaySize(blob.getProperties().getContentLength());
+ String lastModifiedDate = blob.getProperties().getLastModified().format(DateTimeFormatter.ofPattern(DATE_FORMAT));
+ return BucketDTO.builder()
+ .bucket(bucket)
+ .object(blob.getName())
+ .lastModifiedDate(lastModifiedDate)
+ .size(size)
+ .build();
}
}
diff --git a/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/service/impl/gcp/BucketServiceGcpImpl.java b/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/service/impl/gcp/BucketServiceGcpImpl.java
index 1c17142..a245850 100644
--- a/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/service/impl/gcp/BucketServiceGcpImpl.java
+++ b/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/service/impl/gcp/BucketServiceGcpImpl.java
@@ -29,10 +29,9 @@ import com.google.cloud.storage.Bucket;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageOptions;
import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.io.IOUtils;
-import java.io.ByteArrayOutputStream;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
@@ -58,51 +57,57 @@ public class BucketServiceGcpImpl implements BucketService {
}
@Override
- public void uploadObject(String bucket, String object, InputStream stream) {
+ public void uploadObject(String bucket, String object, InputStream stream, long fileSize) {
+ log.info("Uploading file {} to bucket {}", object, bucket);
try {
Storage storage = StorageOptions.getDefaultInstance().getService();
BlobId blobId = BlobId.of(bucket, object);
BlobInfo blobInfo = BlobInfo.newBuilder(blobId).build();
- storage.create(blobInfo, IOUtils.toByteArray(stream));
+ storage.create(blobInfo, stream);
} catch (Exception e) {
log.error("Cannot upload object {} to bucket {}. Reason: {}", object, bucket, e.getMessage());
throw new DlabException(String.format("Cannot upload object %s to bucket %s. Reason: %s", object, bucket, e.getMessage()));
}
+ log.info("Finished uploading file {} to bucket {}", object, bucket);
}
@Override
- public byte[] downloadObject(String bucket, String object) {
- try {
+ public void downloadObject(String bucket, String object, HttpServletResponse resp) {
+ log.info("Downloading file {} from bucket {}", object, bucket);
+ try (ServletOutputStream outputStream = resp.getOutputStream()) {
Storage storage = StorageOptions.getDefaultInstance().getService();
Blob blob = storage.get(BlobId.of(bucket, object));
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- blob.downloadTo(outputStream); //todo add check for blob != null and throw exception
- return outputStream.toByteArray();
+ blob.downloadTo(outputStream);
+ log.info("Finished downloading file {} from bucket {}", object, bucket);
} catch (Exception e) {
log.error("Cannot download object {} from bucket {}. Reason: {}", object, bucket, e.getMessage());
throw new DlabException(String.format("Cannot download object %s from bucket %s. Reason: %s", object, bucket, e.getMessage()));
}
+ log.info("Finished downloading file {} from bucket {}", object, bucket);
}
@Override
- public void deleteObject(String bucket, String object) {
+ public void deleteObjects(String bucket, List<String> objects) {
try {
Storage storage = StorageOptions.getDefaultInstance().getService();
- storage.delete(bucket, object);
+ List<BlobId> blobIds = objects
+ .stream()
+ .map(o -> BlobId.of(bucket, o))
+ .collect(Collectors.toList());
+ storage.delete(blobIds);
} catch (Exception e) {
- log.error("Cannot delete object {} from bucket {}. Reason: {}", object, bucket, e.getMessage());
- throw new DlabException(String.format("Cannot delete object %s from bucket %s. Reason: %s", object, bucket, e.getMessage()));
+ log.error("Cannot delete objects {} from bucket {}. Reason: {}", objects, bucket, e.getMessage());
+ throw new DlabException(String.format("Cannot delete objects %s from bucket %s. Reason: %s", objects, bucket, e.getMessage()));
}
}
private BucketDTO toBucketDTO(BlobInfo blobInfo) {
- final String size = FileUtils.byteCountToDisplaySize(blobInfo.getSize());
Date date = new Date(blobInfo.getUpdateTime());
SimpleDateFormat formatter = new SimpleDateFormat(DATE_FORMAT);
return BucketDTO.builder()
.bucket(blobInfo.getBucket())
.object(blobInfo.getName())
- .size(size)
+ .size(String.valueOf(blobInfo.getSize()))
.lastModifiedDate(formatter.format(date))
.build();
}
diff --git a/services/self-service/pom.xml b/services/self-service/pom.xml
index 382dc7c..f21caf9 100644
--- a/services/self-service/pom.xml
+++ b/services/self-service/pom.xml
@@ -186,10 +186,20 @@
</dependency>
<dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ <version>2.6</version>
+ </dependency>
+ <dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
+ <dependency>
+ <groupId>commons-fileupload</groupId>
+ <artifactId>commons-fileupload</artifactId>
+ <version>${commons-fileupload.version}</version>
+ </dependency>
</dependencies>
<build>
diff --git a/services/self-service/self-service.yml b/services/self-service/self-service.yml
index f4117ff..de7dbc8 100644
--- a/services/self-service/self-service.yml
+++ b/services/self-service/self-service.yml
@@ -46,7 +46,7 @@ checkEnvStatusTimeout: 5m
# Restrict access to DLab features using roles policy
rolePolicyEnabled: true
# Default access to DLab features using roles policy
-roleDefaultAccess: true
+roleDefaultAccess: false
# Set to true to enable the scheduler of billing report.
billingSchedulerEnabled: false
diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/dao/ExploratoryDAO.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/dao/ExploratoryDAO.java
index fc44569..8c5a7ce 100644
--- a/services/self-service/src/main/java/com/epam/dlab/backendapi/dao/ExploratoryDAO.java
+++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/dao/ExploratoryDAO.java
@@ -113,8 +113,8 @@ public class ExploratoryDAO extends BaseDAO {
* @param user name
* @return list of user resources
*/
- public Iterable<Document> findExploratory(String user) {
- return find(USER_INSTANCES, eq(USER, user),
+ public Iterable<Document> findExploratories(String user, String project) {
+ return find(USER_INSTANCES, and(eq(USER, user), eq(PROJECT, project)),
fields(exclude(ExploratoryLibDAO.EXPLORATORY_LIBS, ExploratoryLibDAO.COMPUTATIONAL_LIBS, SCHEDULER_DATA,
EXPLORATORY_USER, EXPLORATORY_PASS)));
}
diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/dao/UserRoleDaoImpl.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/dao/UserRoleDaoImpl.java
index 5bc845a..e95a41b 100644
--- a/services/self-service/src/main/java/com/epam/dlab/backendapi/dao/UserRoleDaoImpl.java
+++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/dao/UserRoleDaoImpl.java
@@ -55,6 +55,9 @@ import static java.util.stream.Collectors.toList;
@Singleton
public class UserRoleDaoImpl extends BaseDAO implements UserRoleDao {
private static final ObjectMapper MAPPER = new ObjectMapper();
+ private static final String[] DEFAULT_AWS_SHAPES = {"nbShapes_t2.medium_fetching", "compShapes_c4.xlarge_fetching"};
+ private static final String[] DEFAULT_GCP_SHAPES = {"compShapes_n1-standard-2_fetching", "nbShapes_n1-standard-2_fetching"};
+ private static final String[] DEFAULT_AZURE_SHAPES = {"nbShapes_Standard_E4s_v3_fetching", "compShapes_Standard_E4s_v3_fetching"};
private static final String ROLES_FILE_FORMAT = "/mongo/%s/mongo_roles.json";
private static final String USERS_FIELD = "users";
private static final String GROUPS_FIELD = "groups";
@@ -128,13 +131,15 @@ public class UserRoleDaoImpl extends BaseDAO implements UserRoleDao {
if (remainingProviders.contains(cloudProviderToBeRemoved)) {
return;
}
-
List<UserRoleDto> remainingRoles = new ArrayList<>();
remainingProviders.forEach(p -> remainingRoles.addAll(getUserRoleFromFile(p)));
- getUserRoleFromFile(cloudProviderToBeRemoved).stream()
+ getUserRoleFromFile(cloudProviderToBeRemoved)
+ .stream()
+ .filter(role -> UserRoleDto.cloudSpecificTypes().contains(role.getType()))
.map(UserRoleDto::getId)
- .filter(u -> remainingRoles.stream()
+ .filter(u -> remainingRoles
+ .stream()
.map(UserRoleDto::getId)
.noneMatch(id -> id.equals(u)))
.forEach(this::remove);
@@ -178,14 +183,11 @@ public class UserRoleDaoImpl extends BaseDAO implements UserRoleDao {
private Set<String> getDefaultShapes(CloudProvider cloudProvider) {
if (cloudProvider == CloudProvider.AWS) {
- return Stream.of("nbShapes_t2.medium_fetching", "compShapes_c4.xlarge_fetching")
- .collect(Collectors.toSet());
+ return Stream.of(DEFAULT_AWS_SHAPES).collect(Collectors.toSet());
} else if (cloudProvider == CloudProvider.GCP) {
- return Stream.of("compShapes_n1-standard-2_fetching", "nbShapes_n1-standard-2_fetching")
- .collect(Collectors.toSet());
+ return Stream.of(DEFAULT_GCP_SHAPES).collect(Collectors.toSet());
} else if (cloudProvider == CloudProvider.AZURE) {
- return Stream.of("nbShapes_Standard_E4s_v3_fetching", "compShapes_Standard_E4s_v3_fetching")
- .collect(Collectors.toSet());
+ return Stream.of(DEFAULT_AZURE_SHAPES).collect(Collectors.toSet());
} else {
throw new DlabException("Unsupported cloud provider " + cloudProvider);
}
diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/BucketResource.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/BucketResource.java
index 7198e35..6ffc7f9 100644
--- a/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/BucketResource.java
+++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/BucketResource.java
@@ -20,21 +20,28 @@
package com.epam.dlab.backendapi.resources;
import com.epam.dlab.auth.UserInfo;
+import com.epam.dlab.backendapi.resources.dto.BucketDeleteDTO;
import com.epam.dlab.backendapi.service.BucketService;
+import com.epam.dlab.exceptions.DlabException;
import com.google.inject.Inject;
import io.dropwizard.auth.Auth;
import lombok.extern.slf4j.Slf4j;
-import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
-import org.glassfish.jersey.media.multipart.FormDataParam;
+import org.apache.commons.fileupload.FileItemIterator;
+import org.apache.commons.fileupload.FileItemStream;
+import org.apache.commons.fileupload.servlet.ServletFileUpload;
+import org.apache.commons.fileupload.util.Streams;
import javax.annotation.security.RolesAllowed;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@@ -44,6 +51,11 @@ import java.nio.file.Paths;
@Path("/bucket")
@Slf4j
public class BucketResource {
+ private static final String OBJECT_FORM_FIELD = "object";
+ private static final String BUCKET_FORM_FIELD = "bucket";
+ private static final String ENDPOINT_FORM_FIELD = "endpoint";
+ private static final String SIZE_FORM_FIELD = "size";
+
private final BucketService bucketService;
@Inject
@@ -67,13 +79,8 @@ public class BucketResource {
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Produces(MediaType.APPLICATION_JSON)
@RolesAllowed("/api/bucket/upload")
- public Response uploadObject(@Auth UserInfo userInfo,
- @FormDataParam("object") String object,
- @FormDataParam("bucket") String bucket,
- @FormDataParam("endpoint") String endpoint,
- @FormDataParam("file") InputStream fileInputStream,
- @FormDataParam("file") FormDataContentDisposition fileMetaData) {
- bucketService.uploadObjects(userInfo, bucket, object, endpoint, fileInputStream);
+ public Response uploadObject(@Auth UserInfo userInfo, @Context HttpServletRequest request) {
+ upload(userInfo, request);
return Response.ok().build();
}
@@ -82,25 +89,62 @@ public class BucketResource {
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_OCTET_STREAM)
@RolesAllowed("/api/bucket/download")
- public Response downloadObject(@Auth UserInfo userInfo,
+ public Response downloadObject(@Auth UserInfo userInfo, @Context HttpServletResponse resp,
@PathParam("bucket") String bucket,
@PathParam("object") String object,
@PathParam("endpoint") String endpoint) {
- return Response.ok(bucketService.downloadObject(userInfo, bucket, object, endpoint))
+ bucketService.downloadObject(userInfo, bucket, object, endpoint, resp);
+ return Response.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + Paths.get(object).getFileName() + "\"")
.build();
}
- @DELETE
- @Path("/{bucket}/object/{object}/endpoint/{endpoint}")
+ @POST
+ @Path("/objects/delete")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@RolesAllowed("/api/bucket/delete")
- public Response deleteObject(@Auth UserInfo userInfo,
- @PathParam("bucket") String bucket,
- @PathParam("object") String object,
- @PathParam("endpoint") String endpoint) {
- bucketService.deleteObject(userInfo, bucket, object, endpoint);
+ public Response deleteObject(@Auth UserInfo userInfo, @Valid BucketDeleteDTO bucketDto) {
+ bucketService.deleteObjects(userInfo, bucketDto.getBucket(), bucketDto.getObjects(), bucketDto.getEndpoint());
return Response.ok().build();
}
+
+ private void upload(UserInfo userInfo, HttpServletRequest request) {
+ String object = null;
+ String bucket = null;
+ String endpoint = null;
+ long fileSize = 0;
+
+ ServletFileUpload upload = new ServletFileUpload();
+ try {
+ FileItemIterator iterStream = upload.getItemIterator(request);
+ while (iterStream.hasNext()) {
+ FileItemStream item = iterStream.next();
+ try (InputStream stream = item.openStream()) {
+ if (item.isFormField()) {
+ if (OBJECT_FORM_FIELD.equals(item.getFieldName())) {
+ object = Streams.asString(stream);
+ }
+ if (BUCKET_FORM_FIELD.equals(item.getFieldName())) {
+ bucket = Streams.asString(stream);
+ }
+ if (ENDPOINT_FORM_FIELD.equals(item.getFieldName())) {
+ endpoint = Streams.asString(stream);
+ }
+ if (SIZE_FORM_FIELD.equals(item.getFieldName())) {
+ fileSize = Long.parseLong(Streams.asString(stream));
+ }
+ } else {
+ bucketService.uploadObjects(userInfo, bucket, object, endpoint, stream, fileSize);
+ }
+ } catch (Exception e) {
+ log.error("Cannot upload object {} to bucket {}. {}", object, bucket, e.getMessage(), e);
+ throw new DlabException(String.format("Cannot upload object %s to bucket %s. %s", object, bucket, e.getMessage()));
+ }
+ }
+ } catch (Exception e) {
+ log.error("User {} cannot upload object {} to bucket {}. {}", userInfo.getName(), object, bucket, e.getMessage(), e);
+ throw new DlabException(String.format("User %s cannot upload object %s to bucket %s. %s", userInfo.getName(), object, bucket, e.getMessage()));
+ }
+ }
}
diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/ExploratoryResource.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/ExploratoryResource.java
index 7b29af1..2a0a701 100644
--- a/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/ExploratoryResource.java
+++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/ExploratoryResource.java
@@ -20,7 +20,6 @@
package com.epam.dlab.backendapi.resources;
import com.epam.dlab.auth.UserInfo;
-import com.epam.dlab.auth.rest.UserSessionDurationAuthorizer;
import com.epam.dlab.backendapi.resources.dto.ExploratoryActionFormDTO;
import com.epam.dlab.backendapi.resources.dto.ExploratoryCreateFormDTO;
import com.epam.dlab.backendapi.roles.RoleType;
@@ -34,7 +33,6 @@ import com.google.inject.Inject;
import io.dropwizard.auth.Auth;
import lombok.extern.slf4j.Slf4j;
-import javax.annotation.security.RolesAllowed;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.ws.rs.Consumes;
@@ -100,7 +98,6 @@ public class ExploratoryResource implements ExploratoryAPI {
* @return Invocation response as JSON string.
*/
@POST
- @RolesAllowed(UserSessionDurationAuthorizer.SHORT_USER_SESSION_DURATION)
public String start(@Auth UserInfo userInfo,
@Valid @NotNull ExploratoryActionFormDTO formDTO) {
log.debug("Starting exploratory environment {} for user {}", formDTO.getNotebookInstanceName(),
diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/azure/ComputationalResourceAzure.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/azure/ComputationalResourceAzure.java
index 29f9794..ca18d14 100644
--- a/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/azure/ComputationalResourceAzure.java
+++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/azure/ComputationalResourceAzure.java
@@ -20,7 +20,6 @@
package com.epam.dlab.backendapi.resources.azure;
import com.epam.dlab.auth.UserInfo;
-import com.epam.dlab.auth.rest.UserSessionDurationAuthorizer;
import com.epam.dlab.backendapi.resources.dto.SparkStandaloneClusterCreateForm;
import com.epam.dlab.backendapi.roles.RoleType;
import com.epam.dlab.backendapi.roles.UserRoles;
@@ -32,7 +31,6 @@ import io.dropwizard.auth.Auth;
import io.swagger.v3.oas.annotations.Parameter;
import lombok.extern.slf4j.Slf4j;
-import javax.annotation.security.RolesAllowed;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.ws.rs.Consumes;
@@ -78,7 +76,6 @@ public class ComputationalResourceAzure {
*/
@PUT
@Path("dataengine")
- @RolesAllowed(UserSessionDurationAuthorizer.SHORT_USER_SESSION_DURATION)
public Response createDataEngine(@Auth UserInfo userInfo,
@Valid @NotNull SparkStandaloneClusterCreateForm form) {
log.debug("Create computational resources for {} | form is {}", userInfo.getName(), form);
diff --git a/services/dlab-model/src/main/java/com/epam/dlab/dto/bucket/BucketDownloadDTO.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/dto/BucketDeleteDTO.java
similarity index 80%
copy from services/dlab-model/src/main/java/com/epam/dlab/dto/bucket/BucketDownloadDTO.java
copy to services/self-service/src/main/java/com/epam/dlab/backendapi/resources/dto/BucketDeleteDTO.java
index b1201e6..9f12a86 100644
--- a/services/dlab-model/src/main/java/com/epam/dlab/dto/bucket/BucketDownloadDTO.java
+++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/dto/BucketDeleteDTO.java
@@ -17,17 +17,22 @@
* under the License.
*/
-package com.epam.dlab.dto.bucket;
+package com.epam.dlab.backendapi.resources.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import org.hibernate.validator.constraints.NotBlank;
+import org.hibernate.validator.constraints.NotEmpty;
+
+import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
-public class BucketDownloadDTO {
+public class BucketDeleteDTO {
@NotBlank(message = "field cannot be empty")
private final String bucket;
@NotBlank(message = "field cannot be empty")
- private final String object;
+ private final String endpoint;
+ @NotEmpty(message = "field cannot be empty")
+ private final List<String> objects;
}
diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/dto/UserResourceInfo.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/dto/UserResourceInfo.java
index ea1198e..5958411 100644
--- a/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/dto/UserResourceInfo.java
+++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/dto/UserResourceInfo.java
@@ -18,88 +18,38 @@
*/
package com.epam.dlab.backendapi.resources.dto;
+import com.epam.dlab.dto.ResourceURL;
import com.epam.dlab.dto.computational.UserComputationalResource;
import com.epam.dlab.model.ResourceEnum;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Builder;
import lombok.Data;
-import java.util.Collections;
import java.util.List;
@Data
+@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
public class UserResourceInfo {
-
+ @JsonProperty
+ private String user;
+ @JsonProperty
+ private String project;
@JsonProperty("resource_type")
private ResourceEnum resourceType;
-
@JsonProperty("resource_name")
private String resourceName;
-
@JsonProperty("shape")
private String resourceShape;
-
@JsonProperty("status")
private String resourceStatus;
-
@JsonProperty("computational_resources")
- private List<UserComputationalResource> computationalResources = Collections.emptyList();
-
- @JsonProperty
- private String user;
- @JsonProperty
- private String project;
-
+ private List<UserComputationalResource> computationalResources;
@JsonProperty("public_ip")
private String ip;
-
@JsonProperty("cloud_provider")
private String cloudProvider;
-
-
- public UserResourceInfo withResourceType(ResourceEnum resourceType) {
- setResourceType(resourceType);
- return this;
- }
-
- public UserResourceInfo withResourceName(String resourceName) {
- setResourceName(resourceName);
- return this;
- }
-
- public UserResourceInfo withResourceShape(String resourceShape) {
- setResourceShape(resourceShape);
- return this;
- }
-
- public UserResourceInfo withResourceStatus(String resourceStatus) {
- setResourceStatus(resourceStatus);
- return this;
- }
-
- public UserResourceInfo withCompResources(List<UserComputationalResource> compResources) {
- setComputationalResources(compResources);
- return this;
- }
-
- public UserResourceInfo withUser(String user) {
- setUser(user);
- return this;
- }
-
- public UserResourceInfo withIp(String ip) {
- setIp(ip);
- return this;
- }
-
- public UserResourceInfo withProject(String project) {
- setProject(project);
- return this;
- }
-
- public UserResourceInfo withCloudProvider(String cloudProvider) {
- setCloudProvider(cloudProvider);
- return this;
- }
+ @JsonProperty("exploratory_urls")
+ private List<ResourceURL> exploratoryUrls;
}
diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/dto/UserRoleDto.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/dto/UserRoleDto.java
index 84551af..989ecf4 100644
--- a/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/dto/UserRoleDto.java
+++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/resources/dto/UserRoleDto.java
@@ -25,6 +25,8 @@ import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
+import java.util.Arrays;
+import java.util.List;
import java.util.Set;
@Getter
@@ -42,6 +44,8 @@ public class UserRoleDto {
private Set<String> exploratories;
@JsonProperty("exploratory_shapes")
private Set<String> exploratoryShapes;
+ @JsonProperty("computational_shapes")
+ private Set<String> computationalShapes;
private Set<String> groups;
private enum Type {
@@ -53,4 +57,8 @@ public class UserRoleDto {
BUCKET_BROWSER,
ADMINISTRATION
}
+
+ public static List<Type> cloudSpecificTypes() {
+ return Arrays.asList(Type.NOTEBOOK, Type.COMPUTATIONAL, Type.NOTEBOOK_SHAPE, Type.COMPUTATIONAL_SHAPE);
+ }
}
diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/service/BucketService.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/service/BucketService.java
index 0377969..6e5345b 100644
--- a/services/self-service/src/main/java/com/epam/dlab/backendapi/service/BucketService.java
+++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/service/BucketService.java
@@ -22,15 +22,16 @@ package com.epam.dlab.backendapi.service;
import com.epam.dlab.auth.UserInfo;
import com.epam.dlab.dto.bucket.BucketDTO;
+import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.util.List;
public interface BucketService {
List<BucketDTO> getObjects(UserInfo userInfo, String bucket, String endpoint);
- void uploadObjects(UserInfo userInfo, String bucket, String object, String endpoint, InputStream inputStream);
+ void uploadObjects(UserInfo userInfo, String bucket, String object, String endpoint, InputStream inputStream, long fileSize);
- byte[] downloadObject(UserInfo userInfo, String bucket, String object, String endpoint);
+ void downloadObject(UserInfo userInfo, String bucket, String object, String endpoint, HttpServletResponse resp);
- void deleteObject(UserInfo userInfo, String bucket, String object, String endpoint);
+ void deleteObjects(UserInfo userInfo, String bucket, List<String> objects, String endpoint);
}
diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/BucketServiceImpl.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/BucketServiceImpl.java
index ce72a6f..d305532 100644
--- a/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/BucketServiceImpl.java
+++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/BucketServiceImpl.java
@@ -25,14 +25,19 @@ import com.epam.dlab.backendapi.service.BucketService;
import com.epam.dlab.backendapi.service.EndpointService;
import com.epam.dlab.constants.ServiceConsts;
import com.epam.dlab.dto.bucket.BucketDTO;
+import com.epam.dlab.dto.bucket.BucketDeleteDTO;
import com.epam.dlab.exceptions.DlabException;
import com.epam.dlab.rest.client.RESTService;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.IOUtils;
import org.apache.http.HttpStatus;
import org.glassfish.jersey.media.multipart.FormDataMultiPart;
+import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@@ -50,7 +55,7 @@ public class BucketServiceImpl implements BucketService {
private static final String BUCKET_GET_OBJECTS = "%sbucket/%s";
private static final String BUCKET_UPLOAD_OBJECT = "%sbucket/upload";
private static final String BUCKET_DOWNLOAD_OBJECT = "%sbucket/%s/object/%s/download";
- private static final String BUCKET_DELETE_OBJECT = "%sbucket/%s/object/%s";
+ private static final String BUCKET_DELETE_OBJECT = "%sbucket/objects/delete";
private final EndpointService endpointService;
private final RESTService provisioningService;
@@ -74,10 +79,11 @@ public class BucketServiceImpl implements BucketService {
}
@Override
- public void uploadObjects(UserInfo userInfo, String bucket, String object, String endpoint, InputStream inputStream) {
+ public void uploadObjects(UserInfo userInfo, String bucket, String object, String endpoint, InputStream inputStream, long fileSize) {
+ log.info("Uploading file {} for user {} to bucket {}", object, userInfo.getName(), bucket);
try {
EndpointDTO endpointDTO = endpointService.get(endpoint);
- FormDataMultiPart formData = getFormDataMultiPart(bucket, object, inputStream);
+ FormDataMultiPart formData = getFormDataMultiPart(bucket, object, inputStream, fileSize);
Response response = provisioningService.postForm(String.format(BUCKET_UPLOAD_OBJECT, endpointDTO.getUrl()), userInfo.getAccessToken(), formData, Response.class);
if (response.getStatus() != HttpStatus.SC_OK) {
throw new DlabException(String.format("Something went wrong. Response status is %s ", response.getStatus()));
@@ -86,14 +92,17 @@ public class BucketServiceImpl implements BucketService {
log.error("Cannot upload object {} to bucket {} for user {}, endpoint {}. Reason {}", object, bucket, userInfo.getName(), endpoint, e.getMessage());
throw new DlabException(String.format("Cannot upload object %s to bucket %s for user %s, endpoint %s. Reason %s", object, bucket, userInfo.getName(), endpoint, e.getMessage()));
}
+ log.info("Finished uploading file {} for user {} to bucket {}", object, userInfo.getName(), bucket);
}
@Override
- public byte[] downloadObject(UserInfo userInfo, String bucket, String object, String endpoint) {
- try {
- EndpointDTO endpointDTO = endpointService.get(endpoint);
- return provisioningService.getWithMediaTypes(String.format(BUCKET_DOWNLOAD_OBJECT, endpointDTO.getUrl(), bucket, encodeObject(object)), userInfo.getAccessToken(), byte[].class,
- APPLICATION_JSON, APPLICATION_OCTET_STREAM);
+ public void downloadObject(UserInfo userInfo, String bucket, String object, String endpoint, HttpServletResponse resp) {
+ log.info("Downloading file {} for user {} from bucket {}", object, userInfo.getName(), bucket);
+ EndpointDTO endpointDTO = endpointService.get(endpoint);
+ try (InputStream inputStream = provisioningService.getWithMediaTypes(String.format(BUCKET_DOWNLOAD_OBJECT, endpointDTO.getUrl(), bucket, encodeObject(object)), userInfo.getAccessToken(),
+ InputStream.class, APPLICATION_JSON, APPLICATION_OCTET_STREAM); ServletOutputStream outputStream = resp.getOutputStream()) {
+ IOUtils.copyLarge(inputStream, outputStream);
+ log.info("Finished downloading file {} for user {} from bucket {}", object, userInfo.getName(), bucket);
} catch (Exception e) {
log.error("Cannot upload object {} from bucket {} for user {}, endpoint {}. Reason {}", object, bucket, userInfo.getName(), endpoint, e.getMessage());
throw new DlabException(String.format("Cannot download object %s from bucket %s for user %s, endpoint %s. Reason %s", object, bucket, userInfo.getName(), endpoint, e.getMessage()));
@@ -101,29 +110,31 @@ public class BucketServiceImpl implements BucketService {
}
@Override
- public void deleteObject(UserInfo userInfo, String bucket, String object, String endpoint) {
+ public void deleteObjects(UserInfo userInfo, String bucket, List<String> objects, String endpoint) {
try {
EndpointDTO endpointDTO = endpointService.get(endpoint);
- Response response = provisioningService.delete(String.format(BUCKET_DELETE_OBJECT, endpointDTO.getUrl(), bucket, encodeObject(object)), userInfo.getAccessToken(), Response.class,
- APPLICATION_JSON, APPLICATION_JSON);
+ BucketDeleteDTO bucketDeleteDTO = new BucketDeleteDTO(bucket, objects);
+ Response response = provisioningService.post(String.format(BUCKET_DELETE_OBJECT, endpointDTO.getUrl()), userInfo.getAccessToken(), bucketDeleteDTO, Response.class);
if (response.getStatus() != HttpStatus.SC_OK) {
throw new DlabException(String.format("Something went wrong. Response status is %s ", response.getStatus()));
}
} catch (Exception e) {
- log.error("Cannot delete object {} from bucket {} for user {}, endpoint {}. Reason {}", object, bucket, userInfo.getName(), endpoint, e.getMessage());
- throw new DlabException(String.format("Cannot delete object %s from bucket %s for user %s, endpoint %s. Reason %s", object, bucket, userInfo.getName(), endpoint, e.getMessage()));
+ log.error("Cannot delete objects {} from bucket {} for user {}, endpoint {}. Reason {}", objects, bucket, userInfo.getName(), endpoint, e.getMessage());
+ throw new DlabException(String.format("Cannot delete objects %s from bucket %s for user %s, endpoint %s. Reason %s", objects, bucket, userInfo.getName(), endpoint, e.getMessage()));
}
}
private String encodeObject(String object) throws UnsupportedEncodingException {
- return URLEncoder.encode(object, StandardCharsets.UTF_8.toString());
+ return URLEncoder.encode(object, StandardCharsets.UTF_8.toString()).replace("+", "%20");
}
- private FormDataMultiPart getFormDataMultiPart(String bucket, String object, InputStream inputStream) {
+ private FormDataMultiPart getFormDataMultiPart(String bucket, String object, InputStream inputStream, long fileSize) {
+ StreamDataBodyPart filePart = new StreamDataBodyPart("file", inputStream, object, MediaType.valueOf(APPLICATION_OCTET_STREAM));
FormDataMultiPart formData = new FormDataMultiPart();
- formData.field("file", inputStream, MediaType.valueOf(APPLICATION_OCTET_STREAM));
formData.field("bucket", bucket);
formData.field("object", object);
+ formData.field("file-size", String.valueOf(fileSize));
+ formData.bodyPart(filePart);
return formData;
}
}
diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/EndpointServiceImpl.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/EndpointServiceImpl.java
index 645f84b..fcfe789 100644
--- a/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/EndpointServiceImpl.java
+++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/EndpointServiceImpl.java
@@ -137,9 +137,9 @@ public class EndpointServiceImpl implements EndpointService {
response = provisioningService.get(url + HEALTH_CHECK, userInfo.getAccessToken(), Response.class);
cloudProvider = response.readEntity(CloudProvider.class);
} catch (Exception e) {
- log.error("Cannot connect to url '{}'. {}", url, e.getMessage());
- throw new DlabException(String.format("Cannot connect to url '%s'. %s", url, e.getMessage()));
- }
+ log.error("Cannot connect to url '{}'. {}", url, e.getMessage());
+ throw new DlabException(String.format("Cannot connect to url '%s'.", url));
+ }
if (response.getStatus() != 200) {
log.warn("Endpoint url {} is not valid", url);
throw new ResourceNotFoundException(String.format("Endpoint url '%s' is not valid", url));
diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/EnvironmentServiceImpl.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/EnvironmentServiceImpl.java
index 8b2806b..6815f9d 100644
--- a/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/EnvironmentServiceImpl.java
+++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/EnvironmentServiceImpl.java
@@ -194,10 +194,12 @@ public class EnvironmentServiceImpl implements EnvironmentService {
if (projectDTO.getEndpoints() != null) {
final Stream<UserResourceInfo> edges = projectDTO.getEndpoints()
.stream()
- .map(e -> new UserResourceInfo().withResourceType(ResourceEnum.EDGE_NODE)
- .withResourceStatus(e.getStatus().toString())
- .withProject(projectDTO.getName())
- .withIp(e.getEdgeInfo() != null ? e.getEdgeInfo().getPublicIp() : null));
+ .map(e -> UserResourceInfo.builder()
+ .resourceType(ResourceEnum.EDGE_NODE)
+ .resourceStatus(e.getStatus().toString())
+ .project(projectDTO.getName())
+ .ip(e.getEdgeInfo() != null ? e.getEdgeInfo().getPublicIp() : null)
+ .build());
return Stream.concat(edges, userResources).collect(toList());
} else {
return userResources.collect(toList());
@@ -205,14 +207,17 @@ public class EnvironmentServiceImpl implements EnvironmentService {
}
private UserResourceInfo toUserResourceInfo(UserInstanceDTO userInstance) {
- return new UserResourceInfo().withResourceType(ResourceEnum.NOTEBOOK)
- .withResourceName(userInstance.getExploratoryName())
- .withResourceShape(userInstance.getShape())
- .withResourceStatus(userInstance.getStatus())
- .withCompResources(userInstance.getResources())
- .withUser(userInstance.getUser())
- .withProject(userInstance.getProject())
- .withCloudProvider(userInstance.getCloudProvider());
+ return UserResourceInfo.builder()
+ .resourceType(ResourceEnum.NOTEBOOK)
+ .resourceName(userInstance.getExploratoryName())
+ .resourceShape(userInstance.getShape())
+ .resourceStatus(userInstance.getStatus())
+ .computationalResources(userInstance.getResources())
+ .user(userInstance.getUser())
+ .project(userInstance.getProject())
+ .cloudProvider(userInstance.getCloudProvider())
+ .exploratoryUrls(userInstance.getResourceUrl())
+ .build();
}
private void checkProjectResourceConditions(String project, String action) {
diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/InfrastructureInfoServiceImpl.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/InfrastructureInfoServiceImpl.java
index 08ce7b9..7554e32 100644
--- a/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/InfrastructureInfoServiceImpl.java
+++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/InfrastructureInfoServiceImpl.java
@@ -25,6 +25,7 @@ import com.epam.dlab.backendapi.dao.BillingDAO;
import com.epam.dlab.backendapi.dao.ExploratoryDAO;
import com.epam.dlab.backendapi.domain.BillingReport;
import com.epam.dlab.backendapi.domain.EndpointDTO;
+import com.epam.dlab.backendapi.domain.ProjectDTO;
import com.epam.dlab.backendapi.domain.ProjectEndpointDTO;
import com.epam.dlab.backendapi.resources.dto.HealthStatusEnum;
import com.epam.dlab.backendapi.resources.dto.HealthStatusPageDTO;
@@ -85,37 +86,14 @@ public class InfrastructureInfoServiceImpl implements InfrastructureInfoService
public List<ProjectInfrastructureInfo> getUserResources(UserInfo user) {
log.debug("Loading list of provisioned resources for user {}", user);
try {
- Iterable<Document> documents = expDAO.findExploratory(user.getName());
List<EndpointDTO> allEndpoints = endpointService.getEndpoints();
- return StreamSupport.stream(documents.spliterator(), false)
- .collect(Collectors.groupingBy(d -> d.getString("project")))
- .entrySet()
+ return projectService.getUserProjects(user, false)
.stream()
- .map(e -> {
- List<ProjectEndpointDTO> endpoints = projectService.get(e.getKey()).getEndpoints();
- List<EndpointDTO> endpointResult = allEndpoints.stream()
- .filter(endpoint -> endpoints.stream()
- .anyMatch(endpoint1 -> endpoint1.getName().equals(endpoint.getName())))
- .collect(Collectors.toList());
-
- List<BillingReport> billingData = e.getValue()
- .stream()
- .map(exp ->
- billingService.getExploratoryBillingData(exp.getString("project"), exp.getString("endpoint"),
- exp.getString("exploratory_name"),
- Optional.ofNullable(exp.get("computational_resources")).map(cr -> (List<Document>) cr).get()
- .stream()
- .map(cr -> cr.getString("computational_name"))
- .collect(Collectors.toList()))
- )
- .collect(Collectors.toList());
-
- final Map<String, Map<String, String>> projectEdges =
- endpoints
- .stream()
- .collect(Collectors.toMap(ProjectEndpointDTO::getName, this::getSharedInfo));
- return new ProjectInfrastructureInfo(e.getKey(), billingDAO.getBillingProjectQuoteUsed(e.getKey()),
- projectEdges, e.getValue(), billingData, endpointResult);
+ .map(p -> {
+ Iterable<Document> exploratories = expDAO.findExploratories(user.getName(), p.getName());
+ return new ProjectInfrastructureInfo(p.getName(), billingDAO.getBillingProjectQuoteUsed(p.getName()),
+ getSharedInfo(p.getName()), exploratories, getExploratoryBillingData(exploratories),
+ getEndpoints(allEndpoints, p));
})
.collect(Collectors.toList());
} catch (Exception e) {
@@ -163,6 +141,30 @@ public class InfrastructureInfoServiceImpl implements InfrastructureInfoService
.build();
}
+ private List<BillingReport> getExploratoryBillingData(Iterable<Document> exploratories) {
+ return StreamSupport.stream(exploratories.spliterator(), false)
+ .map(exp ->
+ billingService.getExploratoryBillingData(exp.getString("project"), exp.getString("endpoint"),
+ exp.getString("exploratory_name"),
+ Optional.ofNullable(exp.get("computational_resources")).map(cr -> (List<Document>) cr).get()
+ .stream()
+ .map(cr -> cr.getString("computational_name"))
+ .collect(Collectors.toList()))
+ )
+ .collect(Collectors.toList());
+ }
+
+ private List<EndpointDTO> getEndpoints(List<EndpointDTO> allEndpoints, ProjectDTO projectDTO) {
+ return allEndpoints.stream().filter(endpoint -> projectDTO.getEndpoints().stream()
+ .anyMatch(endpoint1 -> endpoint1.getName().equals(endpoint.getName())))
+ .collect(Collectors.toList());
+ }
+
+ private Map<String, Map<String, String>> getSharedInfo(String name) {
+ return projectService.get(name).getEndpoints().stream()
+ .collect(Collectors.toMap(ProjectEndpointDTO::getName, this::getSharedInfo));
+ }
+
private Map<String, String> getSharedInfo(ProjectEndpointDTO endpointDTO) {
Optional<EdgeInfo> edgeInfo = Optional.ofNullable(endpointDTO.getEdgeInfo());
if (!edgeInfo.isPresent()) {
diff --git a/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/InfrastructureTemplateServiceImpl.java b/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/InfrastructureTemplateServiceImpl.java
index 31f73f3..9f3f05f 100644
--- a/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/InfrastructureTemplateServiceImpl.java
+++ b/services/self-service/src/main/java/com/epam/dlab/backendapi/service/impl/InfrastructureTemplateServiceImpl.java
@@ -90,8 +90,7 @@ public class InfrastructureTemplateServiceImpl implements InfrastructureTemplate
.peek(e -> e.setImage(getSimpleImageName(e.getImage())))
.filter(e -> exploratoryGpuIssuesAzureFilter(e, endpointDTO.getCloudProvider()) &&
UserRoles.checkAccess(user, RoleType.EXPLORATORY, e.getImage(), roles))
- .peek(e -> filterShapes(user, e.getExploratoryEnvironmentShapes(), RoleType.EXPLORATORY_SHAPES,
- roles))
+ .peek(e -> filterShapes(user, e.getExploratoryEnvironmentShapes(), RoleType.EXPLORATORY_SHAPES, roles))
.collect(Collectors.toList());
} catch (DlabException e) {
@@ -100,20 +99,6 @@ public class InfrastructureTemplateServiceImpl implements InfrastructureTemplate
}
}
- /**
- * Removes shapes for which user does not have an access
- *
- * @param user user
- * @param environmentShapes shape types
- * @param roleType
- * @param roles
- */
- private void filterShapes(UserInfo user, Map<String, List<ComputationalResourceShapeDto>> environmentShapes,
- RoleType roleType, Set<String> roles) {
- environmentShapes.forEach((k, v) -> v.removeIf(compResShapeDto ->
- !UserRoles.checkAccess(user, roleType, compResShapeDto.getType(), roles)));
- }
-
@Override
public List<FullComputationalTemplate> getComputationalTemplates(UserInfo user, String project, String endpoint) {
@@ -129,8 +114,7 @@ public class InfrastructureTemplateServiceImpl implements InfrastructureTemplate
return Arrays.stream(array)
.peek(e -> e.setImage(getSimpleImageName(e.getImage())))
- .peek(e -> filterShapes(user, e.getComputationResourceShapes(), RoleType.COMPUTATIONAL_SHAPES,
- user.getRoles()))
+ .peek(e -> filterShapes(user, e.getComputationResourceShapes(), RoleType.COMPUTATIONAL_SHAPES, roles))
.filter(e -> UserRoles.checkAccess(user, RoleType.COMPUTATIONAL, e.getImage(), roles))
.map(comp -> fullComputationalTemplate(comp, endpointDTO.getCloudProvider()))
.collect(Collectors.toList());
@@ -142,6 +126,21 @@ public class InfrastructureTemplateServiceImpl implements InfrastructureTemplate
}
/**
+ * Removes shapes for which user does not have an access
+ *
+ * @param user user
+ * @param environmentShapes shape types
+ * @param roleType
+ * @param roles
+ */
+ private void filterShapes(UserInfo user, Map<String, List<ComputationalResourceShapeDto>> environmentShapes,
+ RoleType roleType, Set<String> roles) {
+ environmentShapes.forEach((k, v) -> v.removeIf(compResShapeDto ->
+ !UserRoles.checkAccess(user, roleType, compResShapeDto.getType(), roles))
+ );
+ }
+
+ /**
* Temporary filter for creation of exploratory env due to Azure issues
*/
private boolean exploratoryGpuIssuesAzureFilter(ExploratoryMetadataDTO e, CloudProvider cloudProvider) {
diff --git a/services/self-service/src/main/resources/mongo/aws/mongo_roles.json b/services/self-service/src/main/resources/mongo/aws/mongo_roles.json
index ee7d97e..f727854 100644
--- a/services/self-service/src/main/resources/mongo/aws/mongo_roles.json
+++ b/services/self-service/src/main/resources/mongo/aws/mongo_roles.json
@@ -325,7 +325,7 @@
},
{
"_id": "bucketBrowserView",
- "description": "Allow to view objects within the bucket",
+ "description": "Allow to view object via bucket browser",
"type": "BUCKET_BROWSER",
"cloud": "GCP",
"pages": [
@@ -337,7 +337,7 @@
},
{
"_id": "bucketBrowserUpload",
- "description": "Allow to upload object to the bucket",
+ "description": "Allow to upload object via bucket browser",
"type": "BUCKET_BROWSER",
"cloud": "GCP",
"pages": [
@@ -349,7 +349,7 @@
},
{
"_id": "bucketBrowserDownload",
- "description": "Allow to download object from the bucket",
+ "description": "Allow to download object via bucket browser",
"type": "BUCKET_BROWSER",
"cloud": "GCP",
"pages": [
@@ -361,7 +361,7 @@
},
{
"_id": "bucketBrowserDelete",
- "description": "Allow to delete object from the bucket",
+ "description": "Allow to delete object via bucket browser",
"type": "BUCKET_BROWSER",
"cloud": "GCP",
"pages": [
diff --git a/services/self-service/src/main/resources/mongo/azure/mongo_roles.json b/services/self-service/src/main/resources/mongo/azure/mongo_roles.json
index 1cd44b1..6b8f829 100644
--- a/services/self-service/src/main/resources/mongo/azure/mongo_roles.json
+++ b/services/self-service/src/main/resources/mongo/azure/mongo_roles.json
@@ -265,7 +265,7 @@
},
{
"_id": "bucketBrowserView",
- "description": "Allow to view objects within the bucket",
+ "description": "Allow to view object via bucket browser",
"type": "BUCKET_BROWSER",
"cloud": "GCP",
"pages": [
@@ -277,7 +277,7 @@
},
{
"_id": "bucketBrowserUpload",
- "description": "Allow to upload object to the bucket",
+ "description": "Allow to upload object via bucket browser",
"type": "BUCKET_BROWSER",
"cloud": "GCP",
"pages": [
@@ -289,7 +289,7 @@
},
{
"_id": "bucketBrowserDownload",
- "description": "Allow to download object from the bucket",
+ "description": "Allow to download object via bucket browser",
"type": "BUCKET_BROWSER",
"cloud": "GCP",
"pages": [
@@ -301,7 +301,7 @@
},
{
"_id": "bucketBrowserDelete",
- "description": "Allow to delete object from the bucket",
+ "description": "Allow to delete object via bucket browser",
"type": "BUCKET_BROWSER",
"cloud": "GCP",
"pages": [
diff --git a/services/self-service/src/main/resources/mongo/gcp/mongo_roles.json b/services/self-service/src/main/resources/mongo/gcp/mongo_roles.json
index 8a36ff4..11e8731 100644
--- a/services/self-service/src/main/resources/mongo/gcp/mongo_roles.json
+++ b/services/self-service/src/main/resources/mongo/gcp/mongo_roles.json
@@ -301,7 +301,7 @@
},
{
"_id": "bucketBrowserView",
- "description": "Allow to view objects within the bucket",
+ "description": "Allow to view object via bucket browser",
"type": "BUCKET_BROWSER",
"cloud": "GCP",
"pages": [
@@ -313,7 +313,7 @@
},
{
"_id": "bucketBrowserUpload",
- "description": "Allow to upload object to the bucket",
+ "description": "Allow to upload object via bucket browser",
"type": "BUCKET_BROWSER",
"cloud": "GCP",
"pages": [
@@ -325,7 +325,7 @@
},
{
"_id": "bucketBrowserDownload",
- "description": "Allow to download object from the bucket",
+ "description": "Allow to download object via bucket browser",
"type": "BUCKET_BROWSER",
"cloud": "GCP",
"pages": [
@@ -337,7 +337,7 @@
},
{
"_id": "bucketBrowserDelete",
- "description": "Allow to delete object from the bucket",
+ "description": "Allow to delete object via bucket browser",
"type": "BUCKET_BROWSER",
"cloud": "GCP",
"pages": [
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.html
index 74ff5af..e3c77b8 100644
--- a/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.html
@@ -97,7 +97,7 @@
</div>
<div class="text-center m-top-30">
<button mat-raised-button type="button" (click)="dialogRef.close()" class="butt action">Cancel</button>
- <button mat-raised-button type="submit" [disabled]="!manageUsersForm.valid" class="butt butt-success"
+ <button mat-raised-button type="submit" [disabled]="!manageUsersForm.valid || isFormChanged" class="butt butt-success"
[ngClass]="{'not-allowed': !manageUsersForm.valid}">Apply</button>
</div>
</mat-list>
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.ts
index f99944f..0b405be 100644
--- a/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/manage-environment/manage-environment-dilog.component.ts
@@ -37,6 +37,8 @@ export class ManageEnvironmentComponent implements OnInit {
public manageTotalsForm: FormGroup;
@Output() manageEnv: EventEmitter<{}> = new EventEmitter();
+ private initialFormState: any;
+ private isFormChanged: boolean = true;
constructor(
@Inject(MAT_DIALOG_DATA) public data: any,
@@ -50,10 +52,12 @@ export class ManageEnvironmentComponent implements OnInit {
this.setProjectsControl();
this.manageUsersForm.controls['total'].setValue(this.data.total.conf_max_budget || '');
this.onFormChange();
+ this.initialFormState = this.manageUsersForm.value;
}
public onFormChange() {
this.manageUsersForm.valueChanges.subscribe(value => {
+ this.isFormChanged = JSON.stringify(this.initialFormState) === JSON.stringify(this.manageUsersForm.value);
if ((this.getCurrentTotalValue() && this.getCurrentTotalValue() >= this.getCurrentUsersTotal())) {
this.manageUsersForm.controls['projects']['controls'].forEach(v => {
v.controls['budget'].setErrors(null);
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.html b/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.html
index b4f726c..8b7e459 100644
--- a/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.html
@@ -60,7 +60,10 @@
<span [hidden]="filtering && filterForm.type.length > 0 && !collapsedFilterRow">more_vert</span>
</i>
</button> </th>
- <td type mat-cell *matCellDef="let element">{{ element.name || element.type }}</td>
+ <td type mat-cell *matCellDef="let element">
+ <span *ngIf="element.name" class="computation" (click)="openNotebookDetails(element)">{{element.name}}</span>
+ <span *ngIf="!element.name">{{element.type}}</span>
+ </td>
</ng-container>
<ng-container matColumnDef="shape">
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.scss b/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.scss
index 30c082d..5d40c3e 100644
--- a/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.scss
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.scss
@@ -42,7 +42,6 @@
.type {
width: 14%;
- padding-left: 20px;
}
.resources {
@@ -119,3 +118,7 @@ table.management {
background: inherit;
}
}
+
+.computation{
+ cursor: pointer;
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.ts b/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.ts
index d0ab9dc..796859a 100644
--- a/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/management-grid/management-grid.component.ts
@@ -28,6 +28,7 @@ import { ConfirmationDialogComponent } from '../../../shared/modal-dialog/confir
import { EnvironmentsDataService } from '../management-data.service';
import { EnvironmentModel, ManagementConfigModel } from '../management.model';
import {ProgressBarService} from '../../../core/services/progress-bar.service';
+import {DetailDialogComponent} from '../../../resources/exploratory/detail-dialog';
export interface ManageAction {
action: string;
@@ -112,7 +113,9 @@ export class ManagementGridComponent implements OnInit {
let filteredData = this.getEnvironmentDataCopy();
const containsStatus = (list, selectedItems) => {
- return list.filter((item: any) => { if (selectedItems.indexOf(item.status) !== -1) return item; });
+ if (list){
+ return list.filter((item: any) => { if (selectedItems.indexOf(item.status) !== -1) return item; });
+ }
};
if (filteredData.length) this.filtering = true;
@@ -131,8 +134,8 @@ export class ManagementGridComponent implements OnInit {
if (config.resources.length > 0 && modifiedResources.length > 0) { item.resources = modifiedResources; }
- if (config.resources.length === 0 && config.type === 'active' ||
- modifiedResources.length >= 0 && config.resources.length > 0 && config.type === 'active') {
+ if (config.resources && config.resources.length === 0 && config.type === 'active' ||
+ modifiedResources && modifiedResources.length >= 0 && config.resources.length > 0 && config.type === 'active') {
item.resources = modifiedResources;
isResources = true;
}
@@ -216,15 +219,27 @@ export class ManagementGridComponent implements OnInit {
if (item.status && statuses.indexOf(item.status.toLowerCase()) === -1) statuses.push(item.status.toLowerCase());
if (item.project && projects.indexOf(item.project) === -1) projects.push(item.project);
if (item.shape && shapes.indexOf(item.shape) === -1) shapes.push(item.shape);
-
- item.computational_resources.map((resource: any) => {
- if (resources.indexOf(resource.status) === -1) resources.push(resource.status);
- resources.sort(SortUtils.statusSort);
- });
+ if (item.computational_resources) {
+ item.computational_resources.map((resource: any) => {
+ if (resources.indexOf(resource.status) === -1) resources.push(resource.status);
+ resources.sort(SortUtils.statusSort);
+ });
+ }
});
this.filterConfiguration = new ManagementConfigModel(users, '', projects, shapes, statuses, resources);
}
+
+ openNotebookDetails(data) {
+ if (!data.exploratory_urls || !data.exploratory_urls.length) {
+ return;
+ }
+ this.dialog.open(DetailDialogComponent, { data:
+ {notebook: data, bucketStatus: {view: true, upload: true, download: true, delete: true}, buckets: [], type: 'environment'},
+ panelClass: 'modal-lg'
+ })
+ .afterClosed().subscribe(() => {});
+ }
}
diff --git a/services/self-service/src/main/resources/webapp/src/app/administration/management/management.model.ts b/services/self-service/src/main/resources/webapp/src/app/administration/management/management.model.ts
index 2b2fdf3..6b0f600 100644
--- a/services/self-service/src/main/resources/webapp/src/app/administration/management/management.model.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/administration/management/management.model.ts
@@ -27,7 +27,8 @@ export class EnvironmentModel {
public ip: string,
public type?: string,
public project?: string,
- public cloud_provider?: string
+ public cloud_provider?: string,
+ public exploratory_urls?: Array<any>
) { }
public static loadEnvironments(data: Array<any>) {
@@ -41,7 +42,8 @@ export class EnvironmentModel {
value.public_ip,
value.resource_type,
value.project,
- value.cloud_provider
+ value.cloud_provider,
+ value.exploratory_urls
));
}
}
diff --git a/services/self-service/src/main/resources/webapp/src/app/app.module.ts b/services/self-service/src/main/resources/webapp/src/app/app.module.ts
index e23e14a..91ec87d 100644
--- a/services/self-service/src/main/resources/webapp/src/app/app.module.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/app.module.ts
@@ -30,17 +30,17 @@ import { AppComponent } from './app.component';
import { AppRoutingModule } from './app.routing.module';
import { LoginModule } from './login/login.module';
-import { LayoutModule } from './layout/layout.module'
+import { LayoutModule } from './layout/layout.module';
import { GuidesModule } from './help';
import { ServicePagesModule } from './service-pages/service-pages.module';
import { ResourcesModule } from './resources/resources.module';
-
-import { ReportingModule } from './reporting/reporting.module';
import { AdministrationModule } from './administration/administration.module';
import { WebterminalModule } from './webterminal';
import { CoreModule } from './core/core.module';
import { SwaggerAPIModule } from './swagger';
+import {ReportsModule} from './reports/reports.module';
+
@NgModule({
declarations: [AppComponent],
@@ -55,8 +55,10 @@ import { SwaggerAPIModule } from './swagger';
ResourcesModule,
GuidesModule,
ServicePagesModule,
- ReportingModule,
+ // ReportingModule,
+
AdministrationModule,
+ ReportsModule,
WebterminalModule,
SwaggerAPIModule,
RouterModule,
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 3c8ae3f..b8792e5 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
@@ -21,12 +21,12 @@ import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { LoginComponent } from './login/login.module';
-import { LayoutComponent } from './layout/layout.component'
+import { LayoutComponent } from './layout/layout.component';
import { ResourcesComponent } from './resources/resources.component';
import { AccessNotebookGuideComponent, PublicKeyGuideComponent } from './help';
import { NotFoundComponent } from './service-pages/not-found/not-found.component';
import { AccessDeniedComponent } from './service-pages/access-denied/access-denied.component';
-import { ReportingComponent } from './reporting/reporting.component';
+import { ReportingComponent } from './reports/reporting/reporting.component';
import { WebterminalComponent } from './webterminal/webterminal.component';
import { ManagementComponent } from './administration/management/management.component';
import { ProjectComponent } from './administration/project/project.component';
@@ -34,6 +34,7 @@ import { RolesComponent } from './administration/roles/roles.component';
import { SwaggerComponent } from './swagger/swagger.component';
import { AuthorizationGuard, CheckParamsGuard, CloudProviderGuard, AdminGuard } from './core/services';
+import {AuditComponent} from './reports/audit/audit.component';
const routes: Routes = [{
path: 'login',
@@ -79,7 +80,12 @@ const routes: Routes = [{
path: 'help/accessnotebookguide',
component: AccessNotebookGuideComponent,
canActivate: [AuthorizationGuard]
- }
+ },
+ {
+ path: 'audit',
+ component: AuditComponent,
+ canActivate: [AuthorizationGuard, AdminGuard],
+ },
]
}, {
path: 'terminal/:id/:endpoint',
diff --git a/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/service/BucketService.java b/services/self-service/src/main/resources/webapp/src/app/core/pipes/convert-file-size/convert-file-size.pipe.ts
similarity index 60%
copy from services/provisioning-service/src/main/java/com/epam/dlab/backendapi/service/BucketService.java
copy to services/self-service/src/main/resources/webapp/src/app/core/pipes/convert-file-size/convert-file-size.pipe.ts
index f3ed208..0bd2f61 100644
--- a/services/provisioning-service/src/main/java/com/epam/dlab/backendapi/service/BucketService.java
+++ b/services/self-service/src/main/resources/webapp/src/app/core/pipes/convert-file-size/convert-file-size.pipe.ts
@@ -17,21 +17,23 @@
* under the License.
*/
-package com.epam.dlab.backendapi.service;
-
-import com.epam.dlab.dto.bucket.BucketDTO;
-
-import java.io.InputStream;
-import java.util.List;
-
-public interface BucketService {
- String DATE_FORMAT = "dd-M-yyyy hh:mm:ss";
-
- List<BucketDTO> getObjects(String bucket);
-
- void uploadObject(String bucket, String object, InputStream stream);
-
- byte[] downloadObject(String bucket, String object);
-
- void deleteObject(String bucket, String object);
+import { Pipe, PipeTransform } from '@angular/core';
+
+@Pipe({ name: 'convertFileSize' })
+
+export class ConvertFileSizePipe implements PipeTransform {
+ transform(bytes: number): any {
+ const sizes = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
+ if (bytes === 0) {
+ return '0 byte';
+ }
+ for (let i = 0; i < sizes.length; i++) {
+ if (bytes <= 1024) {
+ return bytes + ' ' + sizes[i];
+ } else {
+ bytes = parseFloat((bytes / 1024).toFixed(2));
+ }
+ }
+ return bytes;
+ }
}
diff --git a/services/dlab-model/src/main/java/com/epam/dlab/dto/bucket/BucketDownloadDTO.java b/services/self-service/src/main/resources/webapp/src/app/core/pipes/convert-file-size/index.ts
similarity index 65%
copy from services/dlab-model/src/main/java/com/epam/dlab/dto/bucket/BucketDownloadDTO.java
copy to services/self-service/src/main/resources/webapp/src/app/core/pipes/convert-file-size/index.ts
index b1201e6..2080443 100644
--- a/services/dlab-model/src/main/java/com/epam/dlab/dto/bucket/BucketDownloadDTO.java
+++ b/services/self-service/src/main/resources/webapp/src/app/core/pipes/convert-file-size/index.ts
@@ -17,17 +17,14 @@
* under the License.
*/
-package com.epam.dlab.dto.bucket;
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ConvertFileSizePipe } from './convert-file-size.pipe';
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import lombok.Data;
-import org.hibernate.validator.constraints.NotBlank;
+@NgModule({
+ imports: [CommonModule],
+ declarations: [ConvertFileSizePipe],
+ exports: [ConvertFileSizePipe]
+})
-@Data
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class BucketDownloadDTO {
- @NotBlank(message = "field cannot be empty")
- private final String bucket;
- @NotBlank(message = "field cannot be empty")
- private final String object;
-}
+export class ConvertFileSizePipeModule { }
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 0f05bcb..1a80759 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,6 @@ export class AppRoutingService {
}
redirectToHomePage(): void {
- console.log('redirect');
this.router.navigate(['/resources_list']);
}
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 6ac4fc1..7f125b3 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
@@ -77,6 +77,7 @@ export class ApplicationServiceFacade {
private static readonly PROJECT = 'project';
private static readonly ENDPOINT = 'endpoint';
private static readonly ENDPOINT_CONNECTION = 'endpoint_connection';
+ private static readonly AUDIT = 'audit';
private requestRegistry: Dictionary<string>;
@@ -263,7 +264,7 @@ export class ApplicationServiceFacade {
public buildUploadFileToBucket(data): Observable<any> {
return this.buildRequest(HTTPMethod.POST,
this.requestRegistry.Item(ApplicationServiceFacade.BUCKET) + '/upload',
- data);
+ data, { reportProgress: true, observe: 'events' });
}
public buildDownloadFileFromBucket(data) {
@@ -271,12 +272,12 @@ export class ApplicationServiceFacade {
this.requestRegistry.Item(ApplicationServiceFacade.BUCKET),
data, { dataType : 'binary',
processData : false,
- responseType : 'arraybuffer' } );
+ responseType : 'arraybuffer', reportProgress: true, observe: 'events' } );
}
public buildDeleteFileFromBucket(data): Observable<any> {
- return this.buildRequest(HTTPMethod.DELETE,
- this.requestRegistry.Item(ApplicationServiceFacade.BUCKET),
+ return this.buildRequest(HTTPMethod.POST,
+ this.requestRegistry.Item(ApplicationServiceFacade.BUCKET) + '/objects/delete',
data );
}
@@ -627,6 +628,18 @@ export class ApplicationServiceFacade {
null);
}
+ public getAuditList(): Observable<any> {
+ return this.buildRequest(HTTPMethod.GET,
+ this.requestRegistry.Item(ApplicationServiceFacade.AUDIT),
+ null);
+ }
+
+ public postActionToAudit(data): Observable<any> {
+ return this.buildRequest(HTTPMethod.POST,
+ this.requestRegistry.Item(ApplicationServiceFacade.AUDIT),
+ data);
+ }
+
private setupRegistry(): void {
this.requestRegistry = new Dictionary<string>();
@@ -709,6 +722,9 @@ export class ApplicationServiceFacade {
this.requestRegistry.Add(ApplicationServiceFacade.PROJECT, '/api/project');
this.requestRegistry.Add(ApplicationServiceFacade.ENDPOINT, '/api/endpoint');
this.requestRegistry.Add(ApplicationServiceFacade.ENDPOINT_CONNECTION, '/api/endpoint/url/');
+
+ // audit
+ this.requestRegistry.Add(ApplicationServiceFacade.AUDIT, '/api/audit');
}
private buildRequest(method: HTTPMethod, url_path: string, body: any, opt?) {
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/services/audit.service.ts b/services/self-service/src/main/resources/webapp/src/app/core/services/audit.service.ts
new file mode 100644
index 0000000..70d1f17
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/core/services/audit.service.ts
@@ -0,0 +1,44 @@
+import { Injectable } from '@angular/core';
+import {ApplicationServiceFacade} from './applicationServiceFacade.service';
+import {catchError, map} from 'rxjs/operators';
+import {ErrorUtils} from '../util';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class AuditService {
+ constructor(private applicationServiceFacade: ApplicationServiceFacade) {
+ }
+
+ public getAuditData() {
+ return this.applicationServiceFacade
+ .getAuditList()
+ .pipe(
+ map(response => response),
+ catchError(ErrorUtils.handleServiceError));
+ }
+
+ public sendDataToAudit(data) {
+ return this.applicationServiceFacade
+ .postActionToAudit(data)
+ .pipe(
+ map(response => response),
+ catchError(ErrorUtils.handleServiceError));
+ }
+ // return [
+ // {user: 'Dlab-test-user1', action: 'Deleted users from group', project: '', date: new Date().toLocaleString(), info: {name: 'admin', objects: ['user1', 'user2', 'user3', 'Dlab-test-user1', 'Dlab-test-user1', 'Dlab-test-user1', 'Dlab-test-user1', 'Dlab-test-user1', 'Dlab-test-user1']}},
+ // {user: 'Dlab-test-user1', action: 'Created project', project: 'ProjectA', date: new Date().toLocaleString()},
+ // {user: 'Dlab-test-user1', action: 'Created project', project: 'ProjectA', date: new Date().toLocaleString()},
+ // {user: 'Dlab-test-user1', action: 'Created project', project: 'ProjectA', date: new Date().toLocaleString()},
+ // {user: 'Dlab-test-user2', action: 'Created notebook ', project: 'ProjectA', resource: 'Rstudio', date: new Date().toLocaleString()},
+ // {user: 'Dlab-test-user1', action: 'Deleted user to group', project: '', date: new Date().toLocaleString(), info: {name: 'admin', objects: ['user1', 'user2', 'user3', 'Dlab-test-user1', 'Dlab-test-user1', 'Dlab-test-user1', 'Dlab-test-user1', 'Dlab-test-user1', 'Dlab-test-user1']}},
+ // {user: 'Dlab-test-user1', action: 'Stopped notebook', project: 'ProjectA', resource: 'Rstudio', date: new Date().toLocaleString()},
+ // {user: 'Dlab-test-user1', action: 'Started notebook', project: 'ProjectA', resource: 'Rstudio', date: new Date().toLocaleString()},
+ // {user: 'Dlab-test-user1', action: 'Deleted Users from group', project: '', date: new Date().toLocaleString(), info: {name: 'admin', objects: ['user1', 'user2', 'user3', 'Dlab-test-user1', 'Dlab-test-user1', 'Dlab-test-user1', 'Dlab-test-user1', 'Dlab-test-user1', 'Dlab-test-user1']}},
+ // {user: 'Dlab-test-user3', action: 'Created EMR', project: 'ProjectA', resource: 'Rstudio:Emr1', date: new Date().toLocaleString()},
+ // {user: 'Dlab-test-user1', action: 'Created notebook', project: 'ProjectA', resource: 'Rstudio', date: new Date().toLocaleString()},
+ // {user: 'Dlab-test-user1', action: 'Deleted user to group', project: '', date: new Date().toLocaleString(), info: {name: 'admin', objects: ['user1', 'user2', 'user3', 'Dlab-test-user1', 'Dlab-test-user1', 'Dlab-test-user1', 'Dlab-test-user1', 'Dlab-test-user1', 'Dlab-test-user1']}},
+ // {user: 'Dlab-test-user2', action: 'Terminated notebook', project: 'ProjectA', resource: 'Rstudio', date: new Date().toLocaleString()},
+ // ];
+
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/services/bucket-browser.service.ts b/services/self-service/src/main/resources/webapp/src/app/core/services/bucket-browser.service.ts
index c251ed9..8b2c4f6 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/services/bucket-browser.service.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/services/bucket-browser.service.ts
@@ -8,7 +8,7 @@ export class TodoItemNode {
children: TodoItemNode[];
item: string;
id: string;
- size: number;
+ object: any;
}
export class TodoItemFlatNode {
@@ -50,12 +50,10 @@ export class BucketBrowserService {
}
public deleteFile(data) {
- const url = JSON.stringify(data);
return this.applicationServiceFacade
- .buildDeleteFileFromBucket(url)
+ .buildDeleteFileFromBucket(data)
.pipe(
map(response => response),
catchError(ErrorUtils.handleServiceError));
}
-
}
diff --git a/services/dlab-model/src/main/java/com/epam/dlab/dto/bucket/BucketDownloadDTO.java b/services/self-service/src/main/resources/webapp/src/app/core/util/copyPathUtils.ts
similarity index 65%
copy from services/dlab-model/src/main/java/com/epam/dlab/dto/bucket/BucketDownloadDTO.java
copy to services/self-service/src/main/resources/webapp/src/app/core/util/copyPathUtils.ts
index b1201e6..86ff250 100644
--- a/services/dlab-model/src/main/java/com/epam/dlab/dto/bucket/BucketDownloadDTO.java
+++ b/services/self-service/src/main/resources/webapp/src/app/core/util/copyPathUtils.ts
@@ -17,17 +17,18 @@
* under the License.
*/
-package com.epam.dlab.dto.bucket;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import lombok.Data;
-import org.hibernate.validator.constraints.NotBlank;
-
-@Data
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class BucketDownloadDTO {
- @NotBlank(message = "field cannot be empty")
- private final String bucket;
- @NotBlank(message = "field cannot be empty")
- private final String object;
+export class CopyPathUtils {
+ public static copyPath(copyValue) {
+ const selBox = document.createElement('textarea');
+ selBox.style.position = 'fixed';
+ selBox.style.left = '0';
+ selBox.style.top = '0';
+ selBox.style.opacity = '0';
+ selBox.value = copyValue;
+ document.body.appendChild(selBox);
+ selBox.focus();
+ selBox.select();
+ document.execCommand('copy');
+ document.body.removeChild(selBox);
+ }
}
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/util/patterns.ts b/services/self-service/src/main/resources/webapp/src/app/core/util/patterns.ts
index ac9137f..8246bba 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/util/patterns.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/util/patterns.ts
@@ -24,5 +24,6 @@ export const PATTERNS = {
url: '[a-zA-Z0-9.://%#&\\.@:%-_\+~#=]*\.[^\s]*[a-zA-Z0-9]/+',
nodeCountPattern: '^[1-9]\\d*$',
integerRegex: '^[0-9]*$',
+ folderRegex: /^[a-zA-Z0-9!@$%^&*()_+\-=\[\]{};':|,.<>~` ]*$/,
fullUrl: /^(http?|ftp|https):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+([.:])(\d{4}|com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*\/$/
};
diff --git a/services/self-service/src/main/resources/webapp/src/app/core/util/sortUtils.ts b/services/self-service/src/main/resources/webapp/src/app/core/util/sortUtils.ts
index 0a613ad..2573d0d 100644
--- a/services/self-service/src/main/resources/webapp/src/app/core/util/sortUtils.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/core/util/sortUtils.ts
@@ -34,7 +34,11 @@ export class SortUtils {
Object.keys(shapesJson)
.sort((a, b) => sortOrder.indexOf(a) - sortOrder.indexOf(b))
- .forEach(key => { sortedShapes[key] = shapesJson[key]; });
+ .forEach(key => {
+ if (shapesJson[key].length) {
+ sortedShapes[key] = shapesJson[key];
+ }
+ });
return sortedShapes;
}
@@ -48,5 +52,5 @@ export class SortUtils {
public static flatDeep(arr, d = 1) {
return d > 0 ? arr.reduce((acc, val) => acc.concat(Array.isArray(val) ? this.flatDeep(val, d - 1) : val), [])
: arr.slice();
- };
+ }
}
diff --git a/services/self-service/src/main/resources/webapp/src/app/reports/audit/audit-grid/audit-grid.component.html b/services/self-service/src/main/resources/webapp/src/app/reports/audit/audit-grid/audit-grid.component.html
new file mode 100644
index 0000000..d280224
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/reports/audit/audit-grid/audit-grid.component.html
@@ -0,0 +1,147 @@
+<!--
+ ~ 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="audit-table-wrapper">
+ <table mat-table [dataSource]="auditData" class="data-grid audit mat-elevation-z6">
+
+ <ng-container matColumnDef="user">
+ <th mat-header-cell *matHeaderCellDef class="th_user label-header">
+ <div class="label"><span> User</span></div>
+ <button mat-icon-button aria-label="More" class="ar" (click)="toggleFilterRow()">
+ <i class="material-icons">
+ <span *ngIf="filterAuditData.users.length > 0; else user_filtered">filter_list</span>
+ <ng-template #user_filtered>more_vert</ng-template>
+ </i>
+ </button>
+ </th>
+ <td mat-cell *matCellDef="let element"><span class="table-item user-col">{{element.user}}</span></td>
+ <td mat-footer-cell *matFooterCellDef class="table-footer"></td>
+ </ng-container>
+
+ <ng-container matColumnDef="project">
+ <th mat-header-cell *matHeaderCellDef class="th_project label-header">
+ <div class="label"><span class="text">Project</span></div>
+ <button mat-icon-button aria-label="More" class="ar" (click)="toggleFilterRow()">
+ <i class="material-icons">
+ <span *ngIf="filterAuditData.users.length > 0; else user_filtered">filter_list</span>
+ <ng-template #user_filtered>more_vert</ng-template>
+ </i>
+ </button>
+ </th>
+ <td mat-cell *matCellDef="let element"><span class="table-item">{{element.project}}</span></td>
+ <td mat-footer-cell *matFooterCellDef class="table-footer"></td>
+ </ng-container>
+
+ <ng-container matColumnDef="resource">
+ <th mat-header-cell *matHeaderCellDef class="th_resource label-header">
+ <div class="label"><span class="text">Resource</span></div>
+ <button mat-icon-button aria-label="More" class="ar" (click)="toggleFilterRow()">
+ <i class="material-icons">
+ <span *ngIf="filterAuditData.users.length > 0; else user_filtered">filter_list</span>
+ <ng-template #user_filtered>more_vert</ng-template>
+ </i>
+ </button>
+ </th>
+ <td mat-cell *matCellDef="let element"><span class="table-item">{{element.resource}}</span></td>
+ <td mat-footer-cell *matFooterCellDef class="table-footer"></td>
+ </ng-container>
+
+ <ng-container matColumnDef="action">
+ <th mat-header-cell *matHeaderCellDef class="th_action label-header">
+<!-- <div class="sort">-->
+<!-- <div class="sort-arrow up" (click)="sortBy('user', 'down')" [ngClass]="{'active': !!this.active['userdown']}"></div>-->
+<!-- <div class="sort-arrow down" (click)="sortBy('user', 'up')" [ngClass]="{'active': !!this.active['userup']}"></div>-->
+<!-- </div>-->
+ <div class="label">
+ <span class="text"> Action </span>
+ </div>
+ <button mat-icon-button aria-label="More" class="ar" (click)="toggleFilterRow()">
+ <i class="material-icons">
+ <span *ngIf="filterAuditData.actions.length > 0; else action_filtered">filter_list</span>
+ <ng-template #action_filtered>more_vert</ng-template>
+ </i>
+ </button>
+ </th>
+ <td mat-cell *matCellDef=" let element">
+ <div class="action-wrapper">
+ <span>{{element.action}}</span>
+ <div class="audit-info" (click)="openActionInfo(element)" *ngIf="element.info">
+ <i class="material-icons">info</i>
+ </div>
+ </div>
+ </td>
+ <td mat-footer-cell *matFooterCellDef class="table-footer"></td>
+ </ng-container>
+
+ <ng-container matColumnDef="date">
+ <th mat-header-cell *matHeaderCellDef class="th_date label-header">
+<!-- <div class="sort">-->
+<!-- <div class="sort-arrow up" (click)="sortBy('project', 'down')" [ngClass]="{'active': !!this.active['projectdown']}"></div>-->
+<!-- <div class="sort-arrow down" (click)="sortBy('project', 'up')" [ngClass]="{'active': !!this.active['projectup']}"></div>-->
+<!-- </div>-->
+ <div class="label"><span class="text">Date</span></div>
+ </th>
+ <td mat-cell *matCellDef="let element"> {{element.date}} </td>
+ <td mat-footer-cell *matFooterCellDef class="table-footer"></td>
+ </ng-container>
+
+ <ng-container matColumnDef="user-filter">
+ <th mat-header-cell *matHeaderCellDef class="filter-row-item">
+ <multi-select-dropdown *ngIf="filterConfiguration" (selectionChange)="onUpdate($event)" [type]="'users'"
+ [items]="filterConfiguration.users" [model]="filterAuditData.users"></multi-select-dropdown>
+ </th>
+ </ng-container>
+
+ <ng-container matColumnDef="project-filter">
+ <th mat-header-cell *matHeaderCellDef class="filter-row-item">
+ <multi-select-dropdown *ngIf="filterConfiguration" (selectionChange)="onUpdate($event)" [type]="'projects'"
+ [items]="filterConfiguration.users" [model]="filterAuditData.users"></multi-select-dropdown>
+ </th>
+ </ng-container>
+
+ <ng-container matColumnDef="resource-filter">
+ <th mat-header-cell *matHeaderCellDef class="filter-row-item">
+ <multi-select-dropdown *ngIf="filterConfiguration" (selectionChange)="onUpdate($event)" [type]="'resources'"
+ [items]="filterConfiguration.users" [model]="filterAuditData.users"></multi-select-dropdown>
+ </th>
+ </ng-container>
+
+ <ng-container matColumnDef="action-filter">
+ <th mat-header-cell *matHeaderCellDef class="filter-row-item">
+<!-- <multi-select-dropdown *ngIf="filterConfiguration" (selectionChange)="onUpdate($event)" [type]="'actions'"-->
+<!-- [items]="filterConfiguration.actions" [model]="filterAuditData.actions"></multi-select-dropdown>-->
+ </th>
+ </ng-container>
+ <ng-container matColumnDef="date-filter">
+ <th mat-header-cell *matHeaderCellDef class="filter-row-item">
+
+ </th>
+ </ng-container>
+
+ <tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true" class="header-row"></tr>
+
+ <tr [hidden]="!collapseFilterRow" mat-header-row *matHeaderRowDef="displayedFilterColumns; sticky: true"
+ class="filter-row"></tr>
+ <tr mat-row *matRowDef="let row; columns: displayedColumns;" class="content-row"></tr>
+
+<!-- <tr [hidden]="!auditData?.length" mat-footer-row *matFooterRowDef="displayedColumns; sticky: true"-->
+<!-- class="header-row"></tr>-->
+<!-- <tr [hidden]="reportData?.length" mat-footer-row *matFooterRowDef="['placeholder']"></tr>-->
+ </table>
+</section>
diff --git a/services/self-service/src/main/resources/webapp/src/app/reports/audit/audit-grid/audit-grid.component.scss b/services/self-service/src/main/resources/webapp/src/app/reports/audit/audit-grid/audit-grid.component.scss
new file mode 100644
index 0000000..09b1baa
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/reports/audit/audit-grid/audit-grid.component.scss
@@ -0,0 +1,237 @@
+/*!
+ * 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.
+ */
+
+.audit-table-wrapper {
+ width: 100%;
+
+ .audit {
+ width: 100%;
+ min-width: 1100px;
+ overflow: auto;
+ border-collapse: inherit;
+
+ .mat-cell {
+ vertical-align: middle;
+ }
+
+ tr {
+ .th_user {
+ width: 25%;
+ }
+
+ .th_action {
+ width: 20%;
+ }
+
+ .th_date {
+ width: 20%;
+ }
+
+ .th_project{
+ width: 15%;
+ }
+
+ .th_resource{
+ width: 20%;
+ }
+
+ th {
+ padding-right: 5px;
+ z-index: 2 !important;
+
+ &.th_charges {
+ z-index: 3 !important;
+ }
+ }
+
+ td {
+ font-size: 13px;
+ padding-left: 15px;
+
+ &.info {
+ z-index: 1 !important;
+ text-align: center;
+ padding: 40px;
+ }
+ }
+
+ &.filter-row {
+ th {
+ padding: 5px;
+ font-size: 13px;
+ }
+
+ .filter-field {
+ font-size: 13px;
+ }
+
+ }
+
+ &.header-row {
+ th {
+ font-size: 11px;
+
+ .label {
+ padding-left: 0;
+ }
+ }
+ }
+ }
+
+
+ .tags {
+ .label {
+ padding-top: 0;
+ }
+ }
+
+ .service {
+ min-width: 175px;
+ }
+
+ .env_name {
+ width: 16%;
+ min-width: 200px;
+ }
+
+ .th_project {
+ width: 12%;
+ }
+
+ .th_type {
+ width: 10%;
+ min-width: 150px;
+ }
+
+ .th_status {
+ width: 8%;
+ min-width: 150px;
+ }
+
+ .th_charges {
+ width: 10%;
+ min-width: 155px;
+ text-align: right;
+
+ .label {
+ padding-top: 0;
+ }
+ }
+
+ .th_project {
+ min-width: 150px;
+ }
+
+ .tags-col {
+ padding: 5px;
+
+ mat-chip {
+ min-height: 20px;
+ padding: 5px 10px;
+ font-size: 13px;
+ max-width: 110px !important;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ display: inline-block;
+ line-height: 10px;
+ margin: 2px;
+ }
+ }
+
+ .mat-column-charge {
+ text-align: right;
+ }
+
+ .header-row {
+ position: unset;
+
+ .th_charges {
+ padding-top: 0;
+
+ .label {
+ padding-top: 12px;
+ }
+ }
+
+ .label {
+ display: inline-block;
+ padding-top: 13px;
+ vertical-align: super !important;
+
+ .text {
+ padding-left: 15px;
+ }
+ }
+
+ .sort {
+ position: absolute;
+ bottom: 20px;
+
+ &-arrow {
+ width: 6px;
+ height: 6px;
+ border: 3px solid transparent;
+ border-bottom: 3px solid rgba(0, 0, 0, .54);
+ border-left: 3px solid rgba(0, 0, 0, .54);
+ cursor: pointer;
+
+ &.active {
+ border-bottom: 3px solid #35afd5;
+ border-left: 3px solid #35afd5;
+ }
+ }
+
+ .down {
+ transform: rotate(-45deg);
+ }
+
+ .up {
+ transform: rotate(135deg);
+ }
+ }
+ }
+ }
+
+ .action-wrapper{
+ display: flex;
+ align-items: center;
+ .audit-info{
+ color: lightgray;
+ cursor: pointer;
+ margin-left: 5px;
+ }
+ }
+
+ .dashboard_table_body {
+ td:first-child {
+ cursor: default;
+ }
+
+ .dropdown-multiselect {
+ button {
+ font-size: 14px;
+ height: 34px;
+ padding: 7px 20px;
+ }
+ }
+ }
+ .user-col{
+ padding-left: 5px;
+ }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/reports/audit/audit-grid/audit-grid.component.ts b/services/self-service/src/main/resources/webapp/src/app/reports/audit/audit-grid/audit-grid.component.ts
new file mode 100644
index 0000000..d18908c
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/reports/audit/audit-grid/audit-grid.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, Inject, OnInit} from '@angular/core';
+import {FilterAuditModel} from '../filter-audit.model';
+import {NotificationDialogComponent} from '../../../shared/modal-dialog/notification-dialog';
+import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from '@angular/material/dialog';
+
+@Component({
+ selector: 'dlab-audit-grid',
+ templateUrl: './audit-grid.component.html',
+ styleUrls: ['./audit-grid.component.scss'],
+
+})
+export class AuditGridComponent implements OnInit {
+ public auditData: Array<object>;
+ public displayedColumns: string[] = ['user', 'project', 'resource', 'action', 'date'];
+ public displayedFilterColumns: string[] = ['user-filter', 'project-filter', 'resource-filter', 'action-filter', 'date-filter'];
+ public collapseFilterRow: boolean = true;
+ public filterConfiguration: FilterAuditModel = new FilterAuditModel([], [], [], [], '', '');
+ public filterAuditData: FilterAuditModel = new FilterAuditModel([], [], [], [], '', '');
+
+ constructor(
+ public dialogRef: MatDialogRef<AuditInfoDialogComponent>,
+ public dialog: MatDialog
+ ) {
+ }
+
+ ngOnInit() {}
+
+ public refreshAudit(auditData) {
+ this.auditData = auditData;
+ }
+
+ toggleFilterRow(): void {
+ this.collapseFilterRow = !this.collapseFilterRow;
+ }
+
+ onUpdate($event): void {
+ this.filterAuditData[$event.type] = $event.model;
+ }
+
+ openActionInfo(element) {
+ // console.log('Open audit info ' + action.action);
+ this.dialog.open(AuditInfoDialogComponent, { data: {data: element.info, action: element.action}, panelClass: 'modal-sm' });
+ }
+}
+
+@Component({
+ selector: 'audit-info-dialog',
+ template: `
+ <div id="dialog-box">
+ <header class="dialog-header">
+ <h4 class="modal-title">{{data.action}}</h4>
+ <button type="button" class="close" (click)="dialogRef.close()">×</button>
+ </header>
+ <div mat-dialog-content class="content">
+ <ul info-items-list>
+ <li class="info-item">
+ <span class="info-item-title">Group:</span>
+ <span class="info-item-data"> {{data.data.name}}</span>
+ </li>
+ <li class="info-item">
+ <span class="info-item-title">Users:</span>
+ <span class="info-item-data">
+ <span>{{data.data.objects}}</span>
+ </span>
+ </li>
+ </ul>
+ <div class="text-center m-top-30 m-bott-10">
+<!-- <button type="button" class="butt" mat-raised-button (click)="dialogRef.close()">No</button>-->
+<!-- <button type="button" class="butt butt-success" mat-raised-button-->
+<!-- (click)="dialogRef.close(true)">Yes-->
+<!-- </button>-->
+ </div>
+ </div>
+ </div>
+ `,
+ styles: [`
+ .content { color: #718ba6; padding: 20px 50px; font-size: 14px; font-weight: 400; margin: 0; }
+ .info { color: #35afd5; }
+ .info .confirm-dialog { color: #607D8B; }
+ header { display: flex; justify-content: space-between; color: #607D8B; }
+ header h4 i { vertical-align: bottom; }
+ header a i { font-size: 20px; }
+ header a:hover i { color: #35afd5; cursor: pointer; }
+ .plur { font-style: normal; }
+ .scrolling-content{overflow-y: auto; max-height: 200px; }
+ .endpoint { width: 70%; text-align: left; color: #577289;}
+ .status { width: 30%;text-align: left;}
+ .label { font-size: 15px; font-weight: 500; font-family: "Open Sans",sans-serif;}
+ .node { font-weight: 300;}
+ .resource-name { width: 280px;text-align: left; padding: 10px 0;line-height: 26px;}
+ .project { width: 30%;text-align: left; padding: 10px 0;line-height: 26px;}
+ .resource-list{max-width: 100%; margin: 0 auto;margin-top: 20px; }
+ .resource-list-header{display: flex; font-weight: 600; font-size: 16px;height: 48px; border-top: 1px solid #edf1f5; border-bottom: 1px solid #edf1f5; padding: 0 20px;}
+ .resource-list-row{display: flex; border-bottom: 1px solid #edf1f5;padding: 0 20px;}
+ .confirm-resource-terminating{text-align: left; padding: 10px 20px;}
+ .confirm-message{color: #ef5c4b;font-size: 13px;min-height: 18px; text-align: center; padding-top: 20px}
+ .checkbox{margin-right: 5px;vertical-align: middle; margin-bottom: 3px;}
+ label{cursor: pointer}
+ .bottom-message{padding-top: 15px;}
+ .table-header{padding-bottom: 10px;}
+ .info-item{display: flex; justify-content: space-between; padding: 10px 0; width: 100%}
+ .info-item-title{width: 50%}
+ .info-item-data{width: 50%; text-align: left;}
+
+
+ `]
+})
+export class AuditInfoDialogComponent {
+ constructor(
+ public dialogRef: MatDialogRef<AuditInfoDialogComponent>,
+ @Inject(MAT_DIALOG_DATA) public data: any
+ ) {
+ console.log(data);
+ }
+
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/reporting/toolbar/toolbar.component.html b/services/self-service/src/main/resources/webapp/src/app/reports/audit/audit-toolbar/audit-toolbar.component.html
similarity index 86%
copy from services/self-service/src/main/resources/webapp/src/app/reporting/toolbar/toolbar.component.html
copy to services/self-service/src/main/resources/webapp/src/app/reports/audit/audit-toolbar/audit-toolbar.component.html
index 583371e..601a4dd 100644
--- a/services/self-service/src/main/resources/webapp/src/app/reporting/toolbar/toolbar.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/reports/audit/audit-toolbar/audit-toolbar.component.html
@@ -17,8 +17,8 @@
~ under the License.
-->
<section class="toolbar">
- <div class="info_color" *ngIf="reportData">
- <div class="general">
+ <div class="info_color">
+ <div class="general" *ngIf="reportData">
<div><span>Service base name: </span><strong>{{ reportData.sbn }}</strong></div>
<div *ngIf="reportData.tag_resource_id"><span>Resource tag ID:
</span><strong>{{ reportData.tag_resource_id }}</strong></div>
@@ -34,9 +34,9 @@
<ng-daterangepicker [(ngModel)]="value" [options]="options" (ngModelChange)="onChange($event)"></ng-daterangepicker>
</div>
<div class="action-butt">
- <button mat-raised-button class="butt" (click)="export($event)" [disabled]="!reportData?.report_lines.length">
- <i class="material-icons">file_download</i>Export
- </button>
+<!-- <button mat-raised-button class="butt" (click)="export($event)" [disabled]="!reportData?.report_lines.length">-->
+<!-- <i class="material-icons">file_download</i>Export-->
+<!-- </button>-->
<button mat-raised-button class="butt" (click)="rebuild($event)">
<i class="material-icons">autorenew</i>Refresh
</button>
diff --git a/services/self-service/src/main/resources/webapp/src/app/reporting/toolbar/toolbar.component.scss b/services/self-service/src/main/resources/webapp/src/app/reports/audit/audit-toolbar/audit-toolbar.component.scss
similarity index 100%
copy from services/self-service/src/main/resources/webapp/src/app/reporting/toolbar/toolbar.component.scss
copy to services/self-service/src/main/resources/webapp/src/app/reports/audit/audit-toolbar/audit-toolbar.component.scss
diff --git a/services/self-service/src/main/resources/webapp/src/app/reporting/toolbar/toolbar.component.ts b/services/self-service/src/main/resources/webapp/src/app/reports/audit/audit-toolbar/audit-toolbar.component.ts
similarity index 78%
copy from services/self-service/src/main/resources/webapp/src/app/reporting/toolbar/toolbar.component.ts
copy to services/self-service/src/main/resources/webapp/src/app/reports/audit/audit-toolbar/audit-toolbar.component.ts
index 5272e37..87daf36 100644
--- a/services/self-service/src/main/resources/webapp/src/app/reporting/toolbar/toolbar.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/reports/audit/audit-toolbar/audit-toolbar.component.ts
@@ -19,19 +19,19 @@
import { Component, OnInit, AfterViewInit, Output, EventEmitter, ViewEncapsulation, ViewChild } from '@angular/core';
import { NgDateRangePickerOptions } from 'ng-daterangepicker';
-import { DICTIONARY } from '../../../dictionary/global.dictionary';
+import { DICTIONARY } from '../../../../dictionary/global.dictionary';
import {skip} from 'rxjs/operators';
import {Subscription} from 'rxjs';
-import {HealthStatusService} from '../../core/services';
-import {GeneralEnvironmentStatus} from '../../administration/management/management.model';
+import {HealthStatusService} from '../../../core/services';
+import {GeneralEnvironmentStatus} from '../../../administration/management/management.model';
@Component({
- selector: 'dlab-toolbar',
- templateUrl: './toolbar.component.html',
- styleUrls: ['./toolbar.component.scss'],
+ selector: 'audit-toolbar',
+ templateUrl: './audit-toolbar.component.html',
+ styleUrls: ['./audit-toolbar.component.scss'],
encapsulation: ViewEncapsulation.None
})
-export class ToolbarComponent implements OnInit, AfterViewInit {
+export class AuditToolbarComponent implements OnInit, AfterViewInit {
readonly DICTIONARY = DICTIONARY;
value: any;
reportData: any;
@@ -61,13 +61,13 @@ export class ToolbarComponent implements OnInit, AfterViewInit {
}
ngOnInit() {
- if (localStorage.getItem('report_period')) {
- const availableRange = JSON.parse(localStorage.getItem('report_period'));
- this.availablePeriodFrom = availableRange.start_date;
- this.availablePeriodTo = availableRange.end_date; }
- this.subscriptions.add(this.healthStatusService.statusData.pipe(skip(1)).subscribe(result => {
- this.healthStatus = result;
- }));
+ // if (localStorage.getItem('report_period')) {
+ // const availableRange = JSON.parse(localStorage.getItem('report_period'));
+ // this.availablePeriodFrom = availableRange.start_date;
+ // this.availablePeriodTo = availableRange.end_date; }
+ // this.subscriptions.add(this.healthStatusService.statusData.pipe(skip(1)).subscribe(result => {
+ // this.healthStatus = result;
+ // }));
}
ngAfterViewInit() {
@@ -107,7 +107,7 @@ export class ToolbarComponent implements OnInit, AfterViewInit {
this.rebuildReport.emit($event);
}
- export($event): void {
- this.exportReport.emit($event);
- }
+ // export($event): void {
+ // this.exportReport.emit($event);
+ // }
}
diff --git a/services/self-service/src/main/resources/webapp/src/app/reports/audit/audit.component.ts b/services/self-service/src/main/resources/webapp/src/app/reports/audit/audit.component.ts
new file mode 100644
index 0000000..fba41b2
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/reports/audit/audit.component.ts
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+
+import {Component, OnInit, OnDestroy, ViewChild, Inject} from '@angular/core';
+import { ToastrService } from 'ngx-toastr';
+import {HealthStatusService} from '../../core/services';
+import { DICTIONARY} from '../../../dictionary/global.dictionary';
+import {AuditToolbarComponent} from './audit-toolbar/audit-toolbar.component';
+import {AuditGridComponent} from './audit-grid/audit-grid.component';
+import {AuditService} from '../../core/services/audit.service';
+import {Endpoint} from '../../administration/project/project.component';
+import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
+
+
+@Component({
+ selector: 'dlab-reporting',
+ template: `
+ <div class="base-retreat">
+<!-- <dlab-toolbar (rebuildReport)="rebuildBillingReport()"-->
+<!-- (exportReport)="exportBillingReport()"-->
+<!-- (setRangeOption)="setRangeOption($event)">-->
+<!-- </dlab-toolbar>-->
+<!-- <mat-divider></mat-divider>-->
+<!-- <dlab-reporting-grid (filterReport)="filterReport($event)" (resetRangePicker)="resetRangePicker()"></dlab-reporting-grid>-->
+ <audit-toolbar>
+ </audit-toolbar>
+ <mat-divider></mat-divider>
+ <dlab-audit-grid></dlab-audit-grid>
+ </div>
+
+ `,
+ styles: [`
+ footer {
+ position: fixed;
+ left: 0;
+ bottom: 0;
+ width: 100%;
+ background: #a1b7d1;
+ color: #ffffff;
+ text-align: right;
+ padding: 5px 15px;
+ font-size: 18px;
+ box-shadow: 0 9px 18px 15px #f5f5f5;
+ }
+ `]
+})
+export class AuditComponent implements OnInit, OnDestroy {
+ readonly DICTIONARY = DICTIONARY;
+
+ @ViewChild(AuditGridComponent, { static: true }) auditGrid: AuditGridComponent;
+ @ViewChild(AuditToolbarComponent, { static: true }) auditToolbar: AuditToolbarComponent;
+
+ public auditData;
+
+ constructor(
+ private healthStatusService: HealthStatusService,
+ private auditService: AuditService,
+ public toastr: ToastrService,
+ ) { }
+
+ ngOnInit() {
+ this.getEnvironmentHealthStatus();
+ this.buildAuditReport();
+ }
+
+ ngOnDestroy() {
+ }
+
+ public buildAuditReport() {
+ this.auditData = this.auditService.getAuditData().subscribe(auditData => {
+ console.log(auditData);
+ // this.auditGrid.refreshAudit(auditData);
+ });
+
+
+ }
+
+ private getEnvironmentHealthStatus() {
+ this.healthStatusService.getEnvironmentHealthStatus()
+ .subscribe((result: any) => {});
+ }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/reporting/reporting.module.ts b/services/self-service/src/main/resources/webapp/src/app/reports/audit/audit.module.ts
similarity index 65%
copy from services/self-service/src/main/resources/webapp/src/app/reporting/reporting.module.ts
copy to services/self-service/src/main/resources/webapp/src/app/reports/audit/audit.module.ts
index 3067e9a..ae4d245 100644
--- a/services/self-service/src/main/resources/webapp/src/app/reporting/reporting.module.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/reports/audit/audit.module.ts
@@ -16,18 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
-
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { NgDateRangePickerModule } from 'ng-daterangepicker';
-
-import { MaterialModule } from '../shared/material.module';
-import { FormControlsModule } from '../shared/form-controls';
-import { ReportingComponent } from './reporting.component';
-import { KeysPipeModule, LineBreaksPipeModule } from '../core/pipes';
-import { ReportingGridComponent } from './reporting-grid/reporting-grid.component';
-import { ToolbarComponent } from './toolbar/toolbar.component';
+import { MaterialModule } from '../../shared/material.module';
+import { FormControlsModule } from '../../shared/form-controls';
+import { KeysPipeModule, LineBreaksPipeModule } from '../../core/pipes';
+import {AuditComponent} from './audit.component';
+import {AuditGridComponent, AuditInfoDialogComponent} from './audit-grid/audit-grid.component';
+import {AuditToolbarComponent} from './audit-toolbar/audit-toolbar.component';
@NgModule({
imports: [
@@ -40,10 +38,12 @@ import { ToolbarComponent } from './toolbar/toolbar.component';
MaterialModule
],
declarations: [
- ReportingComponent,
- ReportingGridComponent,
- ToolbarComponent
+ AuditGridComponent,
+ AuditToolbarComponent,
+ AuditComponent,
+ AuditInfoDialogComponent
],
- exports: [ReportingComponent]
+ entryComponents: [AuditInfoDialogComponent],
+ exports: [AuditComponent]
})
-export class ReportingModule { }
+export class AuditModule { }
diff --git a/services/self-service/src/main/resources/webapp/src/app/reports/audit/filter-audit.model.ts b/services/self-service/src/main/resources/webapp/src/app/reports/audit/filter-audit.model.ts
new file mode 100644
index 0000000..c26fdf7
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/reports/audit/filter-audit.model.ts
@@ -0,0 +1,25 @@
+export class FilterAuditModel {
+
+ static getDefault(): FilterAuditModel {
+ return new FilterAuditModel([], [],[],[], '', '');
+ }
+
+ constructor(
+ public users: Array<string>,
+ public resource: Array<string>,
+ public project: Array<string>,
+ public actions: Array<string>,
+ public date_start: string,
+ public date_end: string,
+ ) { }
+
+ defaultConfigurations(): void {
+ this.users = [];
+ this.project = [];
+ this.resource = [];
+ this.actions = [];
+ this.date_start = '';
+ this.date_end = '';
+
+ }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/reporting/reporting-grid/reporting-grid.component.html b/services/self-service/src/main/resources/webapp/src/app/reports/reporting/reporting-grid/reporting-grid.component.html
similarity index 99%
rename from services/self-service/src/main/resources/webapp/src/app/reporting/reporting-grid/reporting-grid.component.html
rename to services/self-service/src/main/resources/webapp/src/app/reports/reporting/reporting-grid/reporting-grid.component.html
index 537cbb5..577e2d4 100644
--- a/services/self-service/src/main/resources/webapp/src/app/reporting/reporting-grid/reporting-grid.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/reports/reporting/reporting-grid/reporting-grid.component.html
@@ -22,7 +22,7 @@
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef class="env_name label-header">
- <div class="label"><span class="text"> Environment name</span></div>
+ <div class="label"><span class="text"> Resource name</span></div>
<button mat-icon-button aria-label="More" class="ar" (click)="toggleFilterRow()">
<i class="material-icons">
<span *ngIf="filteredReportData.dlab_id.length > 0; else dlab_id_filtered">filter_list</span>
diff --git a/services/self-service/src/main/resources/webapp/src/app/reporting/reporting-grid/reporting-grid.component.scss b/services/self-service/src/main/resources/webapp/src/app/reports/reporting/reporting-grid/reporting-grid.component.scss
similarity index 100%
rename from services/self-service/src/main/resources/webapp/src/app/reporting/reporting-grid/reporting-grid.component.scss
rename to services/self-service/src/main/resources/webapp/src/app/reports/reporting/reporting-grid/reporting-grid.component.scss
diff --git a/services/self-service/src/main/resources/webapp/src/app/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
similarity index 90%
rename from services/self-service/src/main/resources/webapp/src/app/reporting/reporting-grid/reporting-grid.component.ts
rename to services/self-service/src/main/resources/webapp/src/app/reports/reporting/reporting-grid/reporting-grid.component.ts
index d4e0076..421ad84 100644
--- a/services/self-service/src/main/resources/webapp/src/app/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
@@ -18,13 +18,13 @@
*/
import {Component, OnInit, Output, EventEmitter, ViewChild, Input} from '@angular/core';
-import { ReportingConfigModel } from '../../../dictionary/global.dictionary';
+import { ReportingConfigModel } from '../../../../dictionary/global.dictionary';
@Component({
selector: 'dlab-reporting-grid',
templateUrl: './reporting-grid.component.html',
styleUrls: ['./reporting-grid.component.scss',
- '../../resources/resources-grid/resources-grid.component.scss'],
+ '../../../resources/resources-grid/resources-grid.component.scss'],
})
export class ReportingGridComponent implements OnInit {
@@ -67,15 +67,15 @@ export class ReportingGridComponent implements OnInit {
let report: Array<object>;
if (direction === 'down') {
report = this.reportData.sort((a, b) => {
- if (a[sortItem] === null) a = '';
- if (b[sortItem] === null) b = '';
+ if (a[sortItem] === null) a[sortItem] = '';
+ if (b[sortItem] === null) b[sortItem] = '';
return (a[sortItem] > b[sortItem]) ? 1 : -1;
});
}
if (direction === 'up') {
report = this.reportData.sort((a, b) => {
- if (a[sortItem] === null) a = '';
- if (b[sortItem] === null) b = '';
+ if (a[sortItem] === null) a[sortItem] = '';
+ if (b[sortItem] === null) b[sortItem] = '';
return (a[sortItem] < b[sortItem]) ? 1 : -1 ;
});
}
diff --git a/services/self-service/src/main/resources/webapp/src/app/reporting/reporting.component.ts b/services/self-service/src/main/resources/webapp/src/app/reports/reporting/reporting.component.ts
similarity index 96%
rename from services/self-service/src/main/resources/webapp/src/app/reporting/reporting.component.ts
rename to services/self-service/src/main/resources/webapp/src/app/reports/reporting/reporting.component.ts
index 03f7ca9..a3e7fa2 100644
--- a/services/self-service/src/main/resources/webapp/src/app/reporting/reporting.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/reports/reporting/reporting.component.ts
@@ -20,13 +20,13 @@
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { ToastrService } from 'ngx-toastr';
-import {ApplicationSecurityService, BillingReportService, HealthStatusService} from '../core/services';
+import {ApplicationSecurityService, BillingReportService, HealthStatusService} from '../../core/services';
import { ReportingGridComponent } from './reporting-grid/reporting-grid.component';
import { ToolbarComponent } from './toolbar/toolbar.component';
-import { FileUtils } from '../core/util';
-import { DICTIONARY, ReportingConfigModel } from '../../dictionary/global.dictionary';
-import {ProgressBarService} from '../core/services/progress-bar.service';
+import { FileUtils } from '../../core/util';
+import { DICTIONARY, ReportingConfigModel } from '../../../dictionary/global.dictionary';
+import {ProgressBarService} from '../../core/services/progress-bar.service';
@Component({
selector: 'dlab-reporting',
@@ -156,7 +156,6 @@ export class ReportingComponent implements OnInit, OnDestroy {
types.push(item['resource_type']);
if (item.shape && types.indexOf(item.shape)) {
- console.log(item);
if (item.shape.indexOf('Master') > -1) {
for (let shape of item.shape.split(/(?=Slave)/g)) {
shape = shape.replace('Master: ', '');
diff --git a/services/self-service/src/main/resources/webapp/src/app/reporting/reporting.module.ts b/services/self-service/src/main/resources/webapp/src/app/reports/reporting/reporting.module.ts
similarity index 88%
rename from services/self-service/src/main/resources/webapp/src/app/reporting/reporting.module.ts
rename to services/self-service/src/main/resources/webapp/src/app/reports/reporting/reporting.module.ts
index 3067e9a..2e3ccb6 100644
--- a/services/self-service/src/main/resources/webapp/src/app/reporting/reporting.module.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/reports/reporting/reporting.module.ts
@@ -22,10 +22,10 @@ import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { NgDateRangePickerModule } from 'ng-daterangepicker';
-import { MaterialModule } from '../shared/material.module';
-import { FormControlsModule } from '../shared/form-controls';
+import { MaterialModule } from '../../shared/material.module';
+import { FormControlsModule } from '../../shared/form-controls';
import { ReportingComponent } from './reporting.component';
-import { KeysPipeModule, LineBreaksPipeModule } from '../core/pipes';
+import { KeysPipeModule, LineBreaksPipeModule } from '../../core/pipes';
import { ReportingGridComponent } from './reporting-grid/reporting-grid.component';
import { ToolbarComponent } from './toolbar/toolbar.component';
diff --git a/services/self-service/src/main/resources/webapp/src/app/reporting/toolbar/toolbar.component.html b/services/self-service/src/main/resources/webapp/src/app/reports/reporting/toolbar/toolbar.component.html
similarity index 89%
rename from services/self-service/src/main/resources/webapp/src/app/reporting/toolbar/toolbar.component.html
rename to services/self-service/src/main/resources/webapp/src/app/reports/reporting/toolbar/toolbar.component.html
index 583371e..7979849 100644
--- a/services/self-service/src/main/resources/webapp/src/app/reporting/toolbar/toolbar.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/reports/reporting/toolbar/toolbar.component.html
@@ -17,15 +17,15 @@
~ under the License.
-->
<section class="toolbar">
- <div class="info_color" *ngIf="reportData">
- <div class="general">
+ <div class="info_color" >
+ <div class="general" *ngIf="reportData">
<div><span>Service base name: </span><strong>{{ reportData.sbn }}</strong></div>
<div *ngIf="reportData.tag_resource_id"><span>Resource tag ID:
</span><strong>{{ reportData.tag_resource_id }}</strong></div>
<div class="report-period info_color" *ngIf="availablePeriodFrom && availablePeriodTo">
<span>Available reporting period from:</span>
- <strong>{{ availablePeriodFrom | date }} </strong>
- to: <strong>{{ availablePeriodTo | date }}</strong>
+ <strong>{{ availablePeriodFrom.join('/') | date }} </strong>
+ to: <strong>{{ availablePeriodTo.join('/') | date }}</strong>
</div>
</div>
</div>
diff --git a/services/self-service/src/main/resources/webapp/src/app/reporting/toolbar/toolbar.component.scss b/services/self-service/src/main/resources/webapp/src/app/reports/reporting/toolbar/toolbar.component.scss
similarity index 100%
rename from services/self-service/src/main/resources/webapp/src/app/reporting/toolbar/toolbar.component.scss
rename to services/self-service/src/main/resources/webapp/src/app/reports/reporting/toolbar/toolbar.component.scss
diff --git a/services/self-service/src/main/resources/webapp/src/app/reporting/toolbar/toolbar.component.ts b/services/self-service/src/main/resources/webapp/src/app/reports/reporting/toolbar/toolbar.component.ts
similarity index 93%
rename from services/self-service/src/main/resources/webapp/src/app/reporting/toolbar/toolbar.component.ts
rename to services/self-service/src/main/resources/webapp/src/app/reports/reporting/toolbar/toolbar.component.ts
index 5272e37..503ffaa 100644
--- a/services/self-service/src/main/resources/webapp/src/app/reporting/toolbar/toolbar.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/reports/reporting/toolbar/toolbar.component.ts
@@ -19,11 +19,11 @@
import { Component, OnInit, AfterViewInit, Output, EventEmitter, ViewEncapsulation, ViewChild } from '@angular/core';
import { NgDateRangePickerOptions } from 'ng-daterangepicker';
-import { DICTIONARY } from '../../../dictionary/global.dictionary';
+import { DICTIONARY } from '../../../../dictionary/global.dictionary';
import {skip} from 'rxjs/operators';
import {Subscription} from 'rxjs';
-import {HealthStatusService} from '../../core/services';
-import {GeneralEnvironmentStatus} from '../../administration/management/management.model';
+import {HealthStatusService} from '../../../core/services';
+import {GeneralEnvironmentStatus} from '../../../administration/management/management.model';
@Component({
selector: 'dlab-toolbar',
@@ -35,8 +35,8 @@ export class ToolbarComponent implements OnInit, AfterViewInit {
readonly DICTIONARY = DICTIONARY;
value: any;
reportData: any;
- availablePeriodFrom: string;
- availablePeriodTo: string;
+ availablePeriodFrom: [];
+ availablePeriodTo: [];
subscriptions: Subscription = new Subscription();
healthStatus: GeneralEnvironmentStatus;
diff --git a/services/dlab-model/src/main/java/com/epam/dlab/dto/bucket/BucketDownloadDTO.java b/services/self-service/src/main/resources/webapp/src/app/reports/reports.module.ts
similarity index 65%
rename from services/dlab-model/src/main/java/com/epam/dlab/dto/bucket/BucketDownloadDTO.java
rename to services/self-service/src/main/resources/webapp/src/app/reports/reports.module.ts
index b1201e6..f6b1bf9 100644
--- a/services/dlab-model/src/main/java/com/epam/dlab/dto/bucket/BucketDownloadDTO.java
+++ b/services/self-service/src/main/resources/webapp/src/app/reports/reports.module.ts
@@ -17,17 +17,15 @@
* under the License.
*/
-package com.epam.dlab.dto.bucket;
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import {AuditModule} from './audit/audit.module';
+import {ReportingModule} from './reporting/reporting.module';
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import lombok.Data;
-import org.hibernate.validator.constraints.NotBlank;
-@Data
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class BucketDownloadDTO {
- @NotBlank(message = "field cannot be empty")
- private final String bucket;
- @NotBlank(message = "field cannot be empty")
- private final String object;
-}
+@NgModule({
+ imports: [CommonModule, AuditModule, ReportingModule],
+ declarations: [],
+ exports: [AuditModule, ReportingModule]
+})
+export class ReportsModule { }
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.html
index 1de656e..05d2715 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.html
@@ -17,115 +17,250 @@
~ under the License.
-->
-<div class="bucket-browser" id="dialog-box">
+<div class="bucket-browser" id="dialog-box" (click)="closeActions()">
<header class="dialog-header">
<h4 class="modal-title">Bucket browser</h4>
<button type="button" class="close" (click)="dialogRef.close()">×</button>
</header>
- <div class="dialog-content tabs" [hidden]="!path" >
+<!-- <div class="dialog-content tabs">-->
+ <div class="dialog-content tabs">
<div class="submit m-bott-10 m-top-10">
- <span [matTooltip]="'You have not permission to upload data'" matTooltipPosition="above" matTooltipDisabled="{{this.bucketStatus.upload}}">
- <button mat-raised-button type="button" class="butt action" [disabled]="!this.bucketStatus.upload">
- <input [ngClass]="{'not-allowed': !this.bucketStatus.upload}" type="file" (change)="handleFileInput($event)">
- Add file
+ <div class="left-side-butts">
+ <span [matTooltip]="isQueueFull ? 'Previous upload is still in progress, please wait.' : 'You have not permission to upload data'" matTooltipPosition="above" matTooltipDisabled="{{this.bucketStatus.upload && !isQueueFull}}">
+ <button mat-raised-button type="button" class="butt first-btn" [disabled]="!this.bucketStatus.upload || allDisable || isSelectionOpened || !path || isQueueFull" (click)="handleFileInput($event)">
+ <input [ngClass]="{'not-allowed': !this.bucketStatus.upload}" type="file" (change)="handleFileInput($event)" title="" multiple>
+ Upload files
</button>
</span>
<span [matTooltip]="'You have not permission to create folder'" matTooltipPosition="above" matTooltipDisabled="{{this.bucketStatus.upload}}">
<button
mat-raised-button
type="button"
- class="butt action"
- (click)="folderTreeComponent.addNewItem(selectedFolder, '', false)"
- [disabled]="!this.bucketStatus.upload"
+ class="butt"
+ (click)="createFolder(selectedFolder)"
+ [disabled]="!this.bucketStatus.upload || allDisable || !path || isSelectionOpened"
>
Create folder
</button>
</span>
+ <span [matTooltip]="'You have not permission to delete data'" matTooltipPosition="above" matTooltipDisabled="{{this.bucketStatus.delete}}">
+ <button
+ type="button"
+ class="butt"
+ mat-raised-button
+ (click)="fileAction('delete')"
+ [disabled]="(!selected?.length && !selectedFolderForAction?.length) || !this.bucketStatus.delete || allDisable || !path || isSelectionOpened"
+ >
+ Delete
+ </button>
+ </span>
+ <div class="action-wrapper" >
+ <span class="action-button-wrapper">
+ <button
+ type="button" class="butt actions-btn"
+ mat-raised-button
+ [disabled]=" selectedItems?.length !== 1 || allDisable || !path || isSelectionOpened"
+ (click)="toogleActions();$event.stopPropagation()"
+ >
+ Actions <i class="material-icons" >{{ !isActionsOpen ? 'expand_more' : 'expand_less' }}</i>
+ </button>
+ </span>
+ <div class="action-menu" *ngIf="isActionsOpen">
+ <span [matTooltip]="'You have not permission to download data'" matTooltipPosition="above" matTooltipDisabled="{{this.bucketStatus.download}}">
+ <button
+ type="button" class="butt action-menu-item"
+ [ngClass]="{'disabled': !selected?.length || this.selectedItems?.length > 1 || !this.bucketStatus.download || allDisable || isSelectionOpened}"
+ mat-raised-button
+ [disabled]=" !selected?.length || this.selectedItems?.length > 1 || !this.bucketStatus.download || allDisable || isSelectionOpened"
+ (click)="fileAction('download');$event.stopPropagation()"
+ >
+ Download
+ </button>
+ </span>
+ <button
+ type="button" class="butt action-menu-item"
+ mat-raised-button
+ (click)="copyPath();$event.stopPropagation()"
+ >
+ Copy path
+ </button>
+ </div>
+ </div>
+ </div>
+ <button
+ mat-raised-button
+ type="button"
+ class="butt refresh"
+ (click)="refreshBucket()"
+ [disabled]="allDisable"
+ >
+ <i class="material-icons">autorenew</i>Refresh
+ </button>
</div>
- <p class="path"><span>Bucket path:</span><span class="url"> {{path}}</span></p>
- <div class="backet-wrapper" id="scrolling">
- <div class="navigation">
- <dlab-folder-tree (showFolderContent)=onFolderClick($event)> </dlab-folder-tree>
+ <p class="path"><span>Bucket path:</span>
+ <span class="url ellipsis" [ngClass]="{'cursor-not-allow': bucketDataService.emptyFolder}">
+ <span *ngFor="let folder of this.objectPath">
+ <span class="url-icon" *ngIf="this.objectPath.indexOf(folder) !== 0"> <i class="material-icons">
+ chevron_right
+ </i> </span>
+ <span class="url-folder" (click)="folderTreeComponent.showItem(folder);" [ngClass]="{'not-allowed': bucketDataService.emptyFolder}">{{folder.item}}</span>
+ </span>
+ </span>
+ </p>
+ <div class="bucket-wrapper" [ngClass]="{'added-upload': addedFiles.length}" id="scrolling">
+ <div class="backet-selection" [ngClass]="{'opened': isSelectionOpened}">
+ <div class="button-wrapper" [ngClass]="{'cursor-not-allow': bucketDataService.emptyFolder}">
+ <i (click)="toggleBucketSelection()" class="material-icons close" *ngIf="!isSelectionOpened" [ngClass]="{'not-allowed': bucketDataService.emptyFolder}">chevron_right</i>
+ </div>
+ <dlab-bucket-tree
+ [hidden] = "!isSelectionOpened"
+ (emitActiveBucket)=openBucket($event)
+ [buckets]='buckets'
+ [openedBucket] = this.bucketName
+ >
+ </dlab-bucket-tree>
</div>
- <div class="directory">
- <ul class="folder-tree" *ngIf="!addedFiles.length">
- <li *ngFor="let file of folderItems" class="folder-item" >
+ <div class="navigation" [ngClass]="{'selection-opened': isSelectionOpened}" [hidden]="!path">
+ <dlab-folder-tree
+ (showFolderContent)=onFolderClick($event)
+ (disableAll) = dissableAll($event)
+ [folders] = folders
+ [endpoint] = endpoint
+ > </dlab-folder-tree>
+ </div>
+ <div class="directory" [ngClass]="{'selection-opened': isSelectionOpened}" [hidden]="!path">
+ <div class="folder-item t_header">
+ <div class="folder-item-wrapper header-wrapper folder-tree header-item">
+ <div class="name" [ngClass]="{'cursor-not-allow': bucketDataService.emptyFolder}">
+ <span class="th_name" (click)="isFilterVisible = true" *ngIf="!isFilterVisible" [ngClass]="{'not-allowed': bucketDataService.emptyFolder}">Name</span>
+ <div class="filter-files" *ngIf="isFilterVisible">
+ <input _ngcontent-yns-c13=""
+ [(ngModel)]="searchValue"
+ (keyup)=filterObjects()
+ class="form-control filter-field filter-name"
+ placeholder="Filter by name" type="text"
+ >
+ <span><i (click)="closeFilterInput()" class="material-icons close">close</i></span>
+ </div>
- <div class="folder-item-wrapper" *ngIf="file.children" (click)="showItem(file)">
- <div class="name name-folder"><i class="material-icons folder-icon" >folder</i><span>{{file.item}}</span></div>
- <div class="size"></div>
- <div class="progress-wrapper">
- <div class="progres" *ngIf="file.isSelected"><div class="bar"></div></div>
+ </div>
+ <div class="size"><span class="th_size">Size</span></div>
+ <div class="date"><span class="th_date">Last modified</span></div>
+ </div>
+ </div>
+ <ul class="folder-tree" [ngClass]="{'cursor-not-allow': bucketDataService.emptyFolder}">
+ <li *ngFor="let file of folderItems" class="folder-item" [ngClass]="{'not-allowed': bucketDataService.emptyFolder}">
+ <div class="folder-item-wrapper" *ngIf="file.children && file.item" (click)="showItem(file)">
+ <div class="name name-folder">
+<!-- <span *ngIf="this.bucketStatus.delete" class="empty-checkbox" [ngClass]="{'checked': file.isFolderSelected}" (click)="toggleSelectedFile(file, 'folder');$event.stopPropagation()" >-->
+ <span class="empty-checkbox" [ngClass]="{'checked': file.isFolderSelected}" (click)="toggleSelectedFile(file, 'folder');$event.stopPropagation()" >
+ <span class="checked-checkbox" *ngIf="file.isFolderSelected"></span>
+ </span>
+ <i class="material-icons folder-icon folder-name">folder</i>
+ <span class="item-name name-wrap"
+ matTooltip="{{file.item}}"
+ matTooltipPosition="above"
+ matTooltipShowDelay="1000"
+ [matTooltipClass]="'bucket-item-tooltip'"
+ >
+ {{file.item}}
+ </span>
</div>
+ <div class="size size-folder">-</div>
+ <div class="date" *ngIf="!file.isDownloading">-</div>
</div>
-
- <div class="folder-item-wrapper" (click)="toggleSelectedFile(file)" *ngIf="!file.children">
+ <div class="folder-item-wrapper" (click)="toggleSelectedFile(file, 'file')" *ngIf="!file.children && file.item !== 'ا'">
<div class="name name-file">
- <span *ngIf="this.bucketStatus.download || this.bucketStatus.download" class="empty-checkbox" [ngClass]="{'checked': file.isSelected}" (click)="toggleSelectedFile(file);$event.stopPropagation()" >
+
+ <span class="empty-checkbox" [ngClass]="{'checked': file.isSelected, 'not-allowed': file.isDownloading}" (click)="toggleSelectedFile(file, 'file');$event.stopPropagation()" >
<span class="checked-checkbox" *ngIf="file.isSelected"></span>
</span>
- <span class="item-name" [ngClass]="{'removed-checkbox': !this.bucketStatus.download && !this.bucketStatus.download}">{{file.item}}</span>
+ <i class="material-icons folder-icon" >description</i>
+
+ <span
+ class="item-name name-wrap"
+ matTooltip="{{file.item}}"
+ matTooltipPosition="above"
+ matTooltipShowDelay="1000"
+ [matTooltipClass]="'bucket-item-tooltip'"
+ >
+ {{file.item}}
+ </span>
</div>
- <div class="size">{{file.size.size}}</div>
- <div class="size" *ngIf="!file.isDownloading">{{file.size.lastModifiedDate }}</div>
+ <div class="size">{{file.object?.size | convertFileSize}}</div>
+ <div class="date" *ngIf="!file.isDownloading">{{convertDate(file.object?.lastModifiedDate) | date: 'dd/MM/yyyy hh:mm:ss aa' }}</div>
+<!-- <div class="date" *ngIf="!file.isDownloading">{{file.object?.lastModifiedDate }}</div>-->
<div class="progress-wrapper" *ngIf="file.isDownloading">
- <mat-progress-bar mode="indeterminate"></mat-progress-bar>
+ <div class="progres">
+ <span class="progress-bar-text">{{file.progress || 0}}% Downloading...</span>
+ <div class="bar" [ngStyle]="{width: file.progress + '%'}">
+ </div>
+ </div>
</div>
</div>
-
- </li>
- </ul>
- <ul class="folder-tree">
- <li *ngFor="let file of addedFiles" class="folder-item">
- <div class="folder-item-wrapper">
- <div class="name">{{file.item}}</div>
- <div class="size">{{file.size}}MB</div>
- <div class="progress-wrapper">
- <mat-progress-bar mode="indeterminate" *ngIf="isUploading"></mat-progress-bar>
- </div>
- <div (click)="deleteAddedFile(file)"><i class="material-icons close">close</i></div>
- </div>
</li>
</ul>
</div>
+ <div class="loading-block" *ngIf="!path">
+ <div class="uploading">
+ <p>Please wait until DLab loads bucket: <span class="strong">{{bucketName}}</span>...</p>
+ <mat-progress-bar mode="indeterminate"></mat-progress-bar>
+ </div>
+ </div>
</div>
- <div class="text-center m-top-30 m-bott-30">
- <button type="button" class="butt" mat-raised-button (click)="this.dialogRef.close()">Close</button>
- <span [matTooltip]="'You have not permission to download data'" matTooltipPosition="above" matTooltipDisabled="{{this.bucketStatus.download}}">
- <button
- *ngIf="!this.addedFiles.length"
- type="button" class="butt butt-success"
- mat-raised-button
- (click)="fileAction('download')"
- [disabled]="!selected?.length || !this.bucketStatus.download"
- >
- Download
- </button>
- </span>
- <span [matTooltip]="'You have not permission to delete data'" matTooltipPosition="above" matTooltipDisabled="{{this.bucketStatus.delete}}">
- <button
- *ngIf="!this.addedFiles.length"
- type="button"
- class="butt butt-success"
- mat-raised-button
- (click)="fileAction('delete')"
- [disabled]="!selected?.length || !this.bucketStatus.delete"
+ <div class="upload-window" *ngIf="addedFiles.length">
+ <header class="upload-header">
+ <h4 class="modal-title">Upload files</h4>
+ <span class="close" matTooltip="Upload is still in progress, please wait." matTooltipPosition="above" [ngClass]="{'not-allow': isFileUploading}" [matTooltipDisabled]="!isFileUploading">
+ <button type="button" class="close" (click)="closeUploadWindow()" [disabled]="isFileUploading" [ngClass]="{'not-allow': isFileUploading}" >×</button>
+ </span>
- >
- Delete
- </button>
- </span>
-
- <button *ngIf="this.addedFiles.length !== 0" type="button" class="butt butt-success" mat-raised-button (click)="uploadNewFile()">Upload</button>
- </div>
- </div>
+ </header>
+ <ul class="upload-files">
+ <li class="file upload-table-header" *ngIf="addedFiles.length">
+ <div class="name ellipsis" >File</div>
+ <div class="second-block">
+ <div class="upload-path ellipsis">Path</div>
+ <div class="size">Size</div>
+ <div class="state"></div>
+ <div class="remove"></div>
+ </div>
- <div class="loading-block" *ngIf="!path">
- <div class="uploading">
- <p>Please wait until DLab loads bucket: <span class="strong">{{data.bucket}}</span>...</p>
- <mat-progress-bar mode="indeterminate"></mat-progress-bar>
+ </li>
+ <li *ngFor="let file of addedFiles" class="file">
+ <div class="name">
+ <span
+ class="ellipsis"
+ matTooltip="{{file.name}}"
+ [matTooltipClass]="'bucket-item-tooltip'"
+ matTooltipPosition="above"
+ >
+ {{file.name}}
+ </span>
+ </div>
+ <div class="second-block">
+ <div class="upload-path">
+ <span class="ellipsis" matTooltip="{{file.path}}" matTooltipPosition="above" [matTooltipClass]="'bucket-item-tooltip'">{{file.path}}</span>
+ </div>
+ <div class="size">{{file.size | convertFileSize}} </div>
+ <div class="state">
+ <!-- <button mat-raised-button (click)="uploadNewFile(file)" *ngIf="!file.isUploading && !file.uploaded && !file.errorUploading">Upload</button>-->
+ <!-- <mat-progress-bar mode="indeterminate" *ngIf="file.isUploading"></mat-progress-bar>-->
+ <span *ngIf="file.status === 'uploaded'" class="running">Uploaded</span>
+ <div class="progres" *ngIf="file.status === 'uploading'">
+ <span class="progress-bar-text">{{file.progress || 0}}% Uploading...</span>
+ <div class="bar" [ngStyle]="{width: file.progress + '%'}">
+ </div>
+ </div>
+ <span *ngIf="file.status === 'failed'" class="error">Failed</span>
+ <span *ngIf="file.status === 'waiting'" class="stopped">Waiting for uploading...</span>
+ </div>
+ <div class="remove"><i (click)="deleteAddedFile(file)" class="material-icons close">close</i></div>
+ </div>
+ </li>
+ <li id="upload-list"></li>
+ </ul>
</div>
</div>
-
</div>
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.scss b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.scss
index eb0c55e..fbdf05b 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.scss
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.component.scss
@@ -16,47 +16,149 @@
* specific language governing permissions and limitations
* under the License.
*/
+.dialog-header {
+ padding-left: 33px !important;
+}
.bucket-browser {
+ font-size: 14px;
+ font-weight: 400;
+
+
.loading-block{
width: 80%;
- margin: 20% auto 0 auto;
+ margin: 0 auto;
display: flex;
- align-content: center;
+ align-items: center;
justify-content: center;
height: 100%;
.uploading{
width: 100%;
text-align: center;
+ padding-bottom: 50px;
p{
margin-bottom: 20px;
}
}
}
+ .action-wrapper{
+ position: relative;
+
+ .action-button-wrapper{
+ position: relative;
+ width: 160px;
+ }
+
+ .mat-raised-button.butt{
+ margin-bottom: 0;
+
+ &.actions-btn{
+ padding-right: 38px;
+
+ .material-icons{
+ transition: ease-in-out 1s;
+ font-size: 25px;
+ position: absolute;
+ top: 7px;
+ right: 30px;
+ }
+ }
+ }
+
+ .action-menu{
+ position: absolute;
+ margin: 0 10px;
+ text-align: center;
+
+ &-item.mat-raised-button.butt{
+ z-index: 1;
+ margin: 0;
+ box-shadow: 0 2px 1px -1px rgba(0,0,0,.2), 0 0px 0px 0 rgba(0,0,0,.14), 0 1px 5px 0 rgba(0,0,0,.12);
+ width: 160px;
+ padding: 0 20px;
+ border-radius: 0;
+ font-style: normal;
+ font-weight: 600;
+ font-size: 15px;
+ font-family: 'Open Sans', sans-serif;
+ color: #577289;
+ position: relative;
+ overflow: hidden;
+ line-height: 36px;
+ }
+ }
+ }
+
.path{
- margin: 0 4px 10px 4px;
- padding: 4px 4px 4px 20px;
+ margin: 0 4px 30px 0;
+ padding: 4px 4px 4px 0;
color: rgba(0,0,0,.87);
+ overflow: hidden;
+ white-space: nowrap;
+ display: flex;
+
.url{
font-weight: 600;
+ overflow: hidden;
+ display: block;
+ direction: rtl;
+
+ &-folder{
+ padding-left: 5px;
+ padding-right: 5px;
+ color: #00bcd4;
+ cursor: pointer;
+ }
+
+ &-icon i{
+ transform: translateY(2px);
+ font-size: 15px;
+ }
}
}
bottom: 0;
+
.dialog-content{
padding: 0 35px;
}
+
.content-box {
height: 500px;
-
}
.submit{
display: flex;
+ justify-content: space-between;
+ .left-side-butts{
+ display: flex;
+ }
+
.butt{
position: relative;
overflow: hidden;
margin: 10px;
+
+ &.first-btn{
+ margin-left: 0;
+ }
+
+ &.refresh{
+ margin-right: 0;
+ }
+
+ &.action-menu-item{
+ &:hover{
+ color: #00bcd4;
+ background-color: #fafafa;
+ }
+ &.disabled{
+ &:hover{
+ color: #577289;
+ }
+ }
+ }
+
input[type="file"] {
position: absolute;
left: 0;
@@ -67,98 +169,204 @@
}
}
}
+}
- .text-center{
- position: absolute;
- bottom: 3vh;
- left: 0;
- right: 0;
+.navigation-line{
+ display: flex;
+ margin-bottom: 25px;
+
+ .endpoint-select{
+ flex: 1;
+ position: relative;
+ opacity: 0;
}
+
+
}
-.backet-wrapper{
- height: 50vh;
+
+.bucket-wrapper{
+ height: 57vh;
border: 2px solid rgba(0,0,0,.12);
border-radius: 5px;
display: flex;
+ width: 100%;
+
+ &.added-upload{
+ height: 40vh;
+ }
+ .backet-selection{
+ position: relative;
+ width: 2%;
+ border-right: 2px solid rgba(0,0,0,.12);
+ padding-top: 6px;
+ transition: .2s;
+ &.opened{
+ width: 33.3%;
+
+ .button-wrapper {
+ text-align: right;
+ left: auto;
+
+
+ i{
+ padding-right: 3px;
+ }
+ }
+ }
+
+ .button-wrapper {
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 9px;
+ text-align: center;
+ i{
+ cursor: pointer;
+ font-size: 18px;
+
+ &:hover{
+ color: #00bcd4;
+ }
+ }
+ }
+ .buckets-list{
+ color: rgba(0, 0, 0, 0.87);
+ padding-left: 20px;
+ line-height: 24px;
+
+ &-item{
+ &:hover{
+ color: #00bcd4;
+ cursor: pointer;
+ }
+ }
+ }
+ }
+
+ .buckets-list{
+ color: rgba(0, 0, 0, 0.87);
+ padding-left: 20px;
+ line-height: 24px;
+ &-item{
+ &:hover{
+ color: #00bcd4;
+ cursor: pointer;
+ }
+ }
+ }
+ }
.navigation{
- flex: 1;
- border-right: 2px solid rgba(0,0,0,.12);
- max-height: 500px;
+ transition: .2s;
+ width: 31.3%;
+ height: 100%;
overflow: auto;
- .folder-tree{
+ padding-top: 6px;
+ &.selection-opened{
+ width: 66.7%;
+ }
+
+ .folder-tree{
.folder{
line-height: 30px;
}
}
+ .mat-tree-node{
+ min-height: auto;
+ }
}
.directory{
- max-height: 500px;
- overflow: auto;
- flex: 2;
+ width: 66.7%;
+ max-height: 100%;
+ font-size: 14px;
+ font-weight: 400;
+ position: relative;
+ border-left: 2px solid rgba(0,0,0,.12);
+
+ &.selection-opened{
+ display: none;
+ }
+
.folder-tree{
+ overflow-x: auto;
+ overflow-y: overlay;
+ max-height: 100%;
+
.name{
- flex:2;
+ width: 60%;
display: flex;
align-items: center;
+ overflow: hidden;
+
+ .name-wrap {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding-right: 10px;
+ }
+
&-folder{
- span{
- padding-left: 10px;
+ .folder-name{
+ padding-left: 8px;
}
}
+
&-file{
- padding-left: 4px;
+ i{
+ transform: translateX(6px);
+ }
span.item-name{
- padding-left: 14px;
+ padding-left: 4px;
&.removed-checkbox{
- padding-left: 0;
+ padding-left: 4px;
}
}
}
}
+
.size{
- flex:1;
+ width: 15%;
+
+ &-folder{
+ padding-left: 4px;
+ }
+ }
+
+ .date{
+ width: 25%;
+
+ .th_date{
+ font-size: 13px;
+ }
}
+
.progress-wrapper{
flex:1;
- .progres{
- border: 1px solid rgba(0,0,0,.12);
- height: 15px;
- position: relative;
- .bar{
- position: absolute;
- top: 0;
- bottom: 0;
- left: 0;
- width: 0;
- background-color: #00bcd4;
- &.full{
- width: 100%;
- transition: 5s ease-in-out;
- }
- }
- }
}
+
.material-icons.close{
font-size: 15px;
margin: 0 10px;
cursor: pointer;
}
+
.action {
display: flex;
justify-content: space-around;
width: 100%;
+
.add-file {
overflow: hidden;
min-width: 100px ;
height: 30px;
position: relative;
+
input{
position: absolute;
left: 0;
@@ -169,17 +377,74 @@
}
}
}
-
}
}
.folder-tree {
- padding: 20px 30px;
+ padding: 0px 15px;
+ padding-top: 6px;
}
.folder-item{
display: flex;
align-items: center;
+
+ &.t_header{
+ top: -41px;
+
+ position: absolute;
+ left: 0;
+ right: 0;
+
+ .folder-item-wrapper{
+ height: 52px;
+ }
+
+ .th_name{
+ padding-left: 29px;
+ font-size: 14px;
+ cursor: pointer;
+
+ &:hover{
+ color: #00bcd4;
+ }
+ }
+
+ .filter-files{
+ width: 100%;
+ padding: 3px 0;
+ padding-left: 3px;
+ display: flex;
+ align-items: center;
+
+ .filter-name{
+ font-size: 14px;
+ height: 20px;
+ width: 90%;
+ }
+
+ span{
+ transform: translateY(2px);
+
+ &:hover{
+ i{
+ color: #00bcd4;
+ }
+ }
+ }
+ }
+
+ .th_size{
+ font-size: 14px;
+ padding-left: 3px;
+ cursor: auto;
+ }
+
+ .th_date{
+ font-size: 14px !important;
+ cursor: auto;
+ }
+ }
.folder-item-wrapper{
width: 100%;
display: flex;
@@ -187,20 +452,34 @@
align-items: center;
cursor: pointer;
color: rgba(0,0,0,.87);
+
+ &.header-item{
+ cursor: auto;
+
+ .name{
+ margin-left: -3px;
+ }
+
+ }
+
i{
color: rgb(232, 232, 232);
}
- &:hover{
+
+ &:hover:not(.header-wrapper){
color: #00bcd4;
transition: .3s ease-in-out;
i{
color: #00bcd4;
transition: .3s ease-in-out;
}
+
.empty-checkbox{
border-color: #00bcd4
}
+
.progress-wrapper{
+
.progres{
border-color: #00bcd4 !important;
}
@@ -208,19 +487,25 @@
}
}
}
+
+input[type='file'] {
+ opacity:1
}
.empty-checkbox {
+ min-width: 16px;
width: 16px;
height: 16px;
border-radius: 2px;
border: 2px solid lightgrey;
margin-top: 2px;
position: relative;
+
&.checked {
border-color: #35afd5;
background-color: #35afd5;
}
+
.checked-checkbox {
top: 0;
left: 4px;
@@ -241,18 +526,187 @@
.check-box {
background-color: red;
}
+}
+
+.upload-window{
+ margin-top: 2vh;
+ border: 2px solid rgba(0,0,0,.12);
+ border-radius: 5px;
+ background-color: white;
+ height: 15vh;
+ color: black;
+
+ .upload-header{
+ padding-left: 8px;
+ background: #f6fafe;
+ height: 30px;
+ line-height: 30px;
+ position: relative;
+ .modal-title {
+ width: 90%;
+ margin: 0;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ font-weight: 500;
+ color: #455c74;
+ font-size: 13px;
+ background: #f6fafe;
+ }
+
+ .close{
+ position: absolute;
+ top: 0;
+ right: 0;
+ height: 30px;
+ width: 30px;
+ font-size: 20px;
+ font-weight: 300;
+ border: 0;
+ background: none;
+ color: #577289;
+ outline: none;
+ cursor: pointer;
+ transition: all 0.45s ease-in-out;
+
+ &:hover{
+ color: #36afd5;
+ }
+ }
+
+ }
+ .upload-files{
+ padding: 5px 0;
+ height: calc(100% - 30px);
+ overflow: auto;
+ overflow-y: overlay;
+ width: 100%;
+ .file{
+ padding: 2px;
+ display: flex;
+ font-size: 12px;
+ position: relative;
+
+ &.upload-table-header{
+ font-size: 11px;
+ }
+
+ .name{
+ width: 33.3%;
+ padding-left: 5px;
+ position: relative;
+ display: flex;
+ span{
+ position: absolute;
+ width: calc(100% + 30px);
+ }
+ }
+
+ .second-block{
+ width: 66.7%;
+ display: flex;
+ padding: 0 14px 0 17px;
+ .upload-path{
+ width: 60%;
+ padding-left: 24px;
+ padding-right: 1%;
+ display: flex;
+ }
+
+ .size{
+ width: 15%;
+ }
+ .state{
+ width: 22%;
+ display: flex;
+ align-items: center;
+ .mat-raised-button{
+ width: 60px;
+ padding: 5px;
+ border-radius: 0;
+ font-style: normal;
+ font-weight: 600;
+ font-size: 11px;
+ font-family: "Open Sans", sans-serif;
+ color: #577289;
+ line-height: 8px;
+ }
+ }
+ .remove{
+ display: flex;
+ align-items: center;
+ width: 5%;
+ position: absolute;
+ right: 20px;
+ .close{
+ color: #577289;
+ font-size: 12px;
+ cursor: pointer;
+ position: absolute;
+ top: 3px;
+ right: 0;
+ height: 18px;
+ width: 14px;
+ transition: all 0.45s ease-in-out;
+
+ &:hover{
+ color: #f1696e;
+ }
+ }
+ }
+ }
+
+
+ }
+ }
+}
+.events-none{
+ pointer-events: none;
+}
+
+@media only screen and (max-height: 920px) {
+ .bucket-wrapper {
+ height: 55vh;
+ &.added-upload{
+ height: 38vh;
+ }
+ }
}
@media only screen and (max-height: 840px) {
- .backet-wrapper {
- height: 45vh;
+ .bucket-wrapper {
+ height: 53vh;
+ &.added-upload{
+ height: 36vh;
+ }
}
}
-@media only screen and (max-height: 690px) {
- .backet-wrapper {
- height: 40vh;
+@media only screen and (max-height: 760px) {
+ .bucket-wrapper {
+ height: 51vh;
+ &.added-upload{
+ height: 34vh;
+ }
+ }
+}
+
+@media only screen and (max-height: 700px) {
+ .bucket-wrapper {
+ height: 49vh;
+ &.added-upload{
+ height: 32vh;
+ }
+ }
+}
+
+@media only screen and (max-height: 650px) {
+ .bucket-wrapper {
+ height: 47vh;
+ &.added-upload{
+ height: 30vh;
+ }
}
}
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 706a8f1..696835e 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
@@ -21,12 +21,16 @@ import { Component, OnInit, ViewChild, Inject } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { ToastrService } from 'ngx-toastr';
-import { ManageUngitService } from '../../core/services';
+import {ApplicationSecurityService, ManageUngitService, StorageService} from '../../core/services';
import {FolderTreeComponent} from './folder-tree/folder-tree.component';
import {BucketBrowserService, TodoItemNode} from '../../core/services/bucket-browser.service';
import {FileUtils} from '../../core/util';
import {BucketDataService} from './bucket-data.service';
+import {BucketConfirmationDialogComponent} from './bucket-confirmation-dialog/bucket-confirmation-dialog.component';
+import {logger} from 'codelyzer/util/logger';
+import {HttpEventType, HttpResponse} from '@angular/common/http';
+import {CopyPathUtils} from '../../core/util/copyPathUtils';
@Component({
selector: 'dlab-bucket-browser',
@@ -36,17 +40,33 @@ import {BucketDataService} from './bucket-data.service';
export class BucketBrowserComponent implements OnInit {
public addedFiles = [];
public folderItems = [];
+ public originFolderItems = [];
+ public objectPath;
public path = '';
public pathInsideBucket = '';
public bucketName = '';
public endpoint = '';
-
- @ViewChild(FolderTreeComponent, {static: true}) folderTreeComponent;
public selectedFolder: any;
- private isUploading: boolean;
- private selected: any[];
- private uploadForm: FormGroup;
+ public selectedFolderForAction: any;
+ public selected: any[];
public bucketStatus;
+ public allDisable: boolean;
+ public isActionsOpen: boolean;
+ public folders: any[];
+ public selectedItems;
+ public searchValue: string;
+ public isQueueFull: boolean;
+ public refreshTokenLimit = 900000;
+ private isTokenRefreshing = false;
+ @ViewChild(FolderTreeComponent, {static: true}) folderTreeComponent;
+ public isSelectionOpened: any;
+ isFilterVisible: boolean;
+ public buckets;
+ private isFileUploading: boolean;
+ private isFileUploaded: boolean;
+
+
+
constructor(
@Inject(MAT_DIALOG_DATA) public data: any,
@@ -56,19 +76,34 @@ export class BucketBrowserComponent implements OnInit {
private manageUngitService: ManageUngitService,
private _fb: FormBuilder,
private bucketBrowserService: BucketBrowserService,
- private bucketDataService: BucketDataService,
- private formBuilder: FormBuilder
+ public bucketDataService: BucketDataService,
+ private auth: ApplicationSecurityService,
+ private storage: StorageService,
) {
}
ngOnInit() {
- this.bucketDataService.refreshBucketdata(this.data.bucket, this.data.endpoint);
+ this.bucketName = this.data.bucket;
+ this.bucketDataService.refreshBucketdata(this.bucketName, this.data.endpoint);
this.endpoint = this.data.endpoint;
this.bucketStatus = this.data.bucketStatus;
- this.uploadForm = this.formBuilder.group({
- file: ['']
+ this.buckets = this.data.buckets;
+ }
+
+ public getTokenValidTime() {
+ const token = JSON.parse(atob(this.storage.getToken().split('.')[1]));
+ return token.exp * 1000 - new Date().getTime();
+ }
+
+ private refreshToken() {
+ this.isTokenRefreshing = true;
+ this.auth.refreshToken().subscribe(tokens => {
+ this.storage.storeTokens(tokens);
+ this.isTokenRefreshing = false;
+ this.sendFile();
});
+
}
public showItem(item) {
@@ -76,97 +111,333 @@ export class BucketBrowserComponent implements OnInit {
this.folderTreeComponent.showItem(flatItem);
}
- public handleFileInput(event) {
- if (event.target.files.length > 0) {
- const file = event.target.files[0];
- this.uploadForm.get('file').setValue(file);
- const newAddedFiles = Object['values'](event.target.files).map(v => (
- {item: v['name'], 'size': (v['size'] / 1048576).toFixed(2)} as unknown as TodoItemNode ));
- this.addedFiles = [...newAddedFiles];
- }
+ public closeUploadWindow() {
+ this.addedFiles = [];
}
- public toggleSelectedFile(file) {
- // remove if when will be possible download several files
- if (!file.isSelected) {
- this.folderItems.forEach(item => item.isSelected = false);
- }
-
- file.isSelected = !file.isSelected;
- this.selected = this.folderItems.filter(item => item.isSelected);
+ public toggleSelectedFile(file, type) {
+ 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);
+ this.selectedItems = [...this.selected, ...this.selectedFolderForAction];
+ this.isActionsOpen = false;
}
filesPicked(files) {
-
Array.prototype.forEach.call(files, file => {
this.addedFiles.push(file.webkitRelativePath);
});
}
+ public dissableAll(event) {
+ this.allDisable = event;
+ }
+
+ public handleFileInput(event) {
+ const fullFilesList = Object['values'](event.target.files);
+ if (fullFilesList.length > 0) {
+ const files = fullFilesList.filter(v => v.size < 4294967296);
+ const toBigFile = fullFilesList.length !== files.length;
+ const toMany = files.length > 50;
+ if (files.length > 50) {
+ files.length = 50;
+ }
+ if (toBigFile || toMany) {
+ this.dialog.open(BucketConfirmationDialogComponent, {data: {
+ items: {toBig: toBigFile, toMany: toMany}, type: 'uploading-error'
+ } , width: '550px'})
+ .afterClosed().subscribe((res) => {
+ if (res) {
+ if (this.refreshTokenLimit > this.getTokenValidTime()) {
+ this.isTokenRefreshing = true;
+ this.auth.refreshToken().subscribe(v => {
+ this.uploadingQueue(files);
+ this.isTokenRefreshing = false;
+ });
+ } else {
+ this.uploadingQueue(files);
+ }
+ }
+ });
+ } else {
+ if (this.refreshTokenLimit > this.getTokenValidTime()) {
+ this.isTokenRefreshing = true;
+ this.auth.refreshToken().subscribe(v => {
+ this.uploadingQueue(files);
+ this.isTokenRefreshing = false;
+ });
+ } else {
+ this.uploadingQueue(files);
+ }
+ }
+ }
+ event.target.value = '';
+ }
+
+ private async uploadingQueue(files) {
+ if (files.length) {
+ let askForAll = true;
+ let skipAll = false;
+
+ const folderFiles = this.folderItems.filter(v => !v.children).map(v => v.item);
+ for (const file of files) {
+ const existFile = folderFiles.filter(v => v === file['name'])[0];
+ const uploadItem = {
+ name: file['name'],
+ file: file,
+ size: file.size,
+ path: this.path,
+ };
+ if (existFile && askForAll) {
+ const result = await this.openResolveDialog(existFile);
+ if (result) {
+ askForAll = !result.forAll;
+ if (result.forAll && !result.replaceObject) {
+ skipAll = true;
+ }
+ if (result.replaceObject) {
+ this.addedFiles.push(uploadItem);
+ this.uploadNewFile(uploadItem);
+ }
+ }
+ } else if (!existFile || (existFile && !askForAll && !skipAll)) {
+ this.addedFiles.push(uploadItem);
+ this.uploadNewFile(uploadItem);
+ }
+ }
+ }
+ setTimeout(() => {
+ const element = document.querySelector('#upload-list');
+ element && element.scrollIntoView({ block: 'end', behavior: 'smooth' });
+ }, 0);
+ }
+
+ async openResolveDialog(existFile) {
+ const dialog = this.dialog.open(BucketConfirmationDialogComponent, {
+ data: {items: existFile, type: 'upload-dublicat'} , width: '550px'
+ });
+ return dialog.afterClosed().toPromise().then(result => {
+ return Promise.resolve(result);
+ });
+ }
+
public onFolderClick(event) {
+ this.searchValue = '';
+ this.clearSelection();
this.selectedFolder = event.flatNode;
+ if (this.isSelectionOpened) {
+ this.isSelectionOpened = false;
+ }
this.folderItems = event.element ? event.element.children : event.children;
if (this.folderItems) {
- const folders = this.folderItems.filter(v => v.children).sort((a, b) => a.item > b.item ? 1 : -1);
+ this.folders = this.folderItems.filter(v => v.children);
const files = this.folderItems.filter(v => !v.children).sort((a, b) => a.item > b.item ? 1 : -1);
- this.folderItems = [...folders, ...files];
+ this.folderItems = [...this.folders, ...files];
+ 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.bucketName = this.path.substring(0, this.path.indexOf('/')) || this.path;
this.folderItems.forEach(item => item.isSelected = false);
}
+ }
+ public filterObjects() {
+ this.folderItems = this.originFolderItems.filter(v => v.item.toLowerCase().indexOf(this.searchValue.toLowerCase()) !== -1);
}
- public deleteAddedFile(file) {
- this.addedFiles.splice(this.addedFiles.indexOf(file), 1);
+ private clearSelection() {
+ this.folderItems.forEach(item => item.isSelected = false);
+ this.folderItems.forEach(item => item.isFolderSelected = false);
+ this.selected = this.folderItems.filter(item => item.isSelected);
+ this.selectedFolderForAction = this.folderItems.filter(item => item.isFolderSelected);
+ this.selectedItems = [];
}
- private uploadNewFile() {
- const path = `${this.pathInsideBucket}${this.uploadForm.get('file').value.name}`;
+ public deleteAddedFile(file) {
+ if ( file.subscr && file.request) {
+ this.dialog.open(BucketConfirmationDialogComponent, {data: {items: file, type: 'cancel-uploading'} , width: '550px'})
+ .afterClosed().subscribe((res) => {
+ res && file.subscr.unsubscribe();
+ res && this.addedFiles.splice(this.addedFiles.indexOf(file), 1);
+ this.isFileUploading = !!this.addedFiles.filter(v => v.status === 'uploading').length;
+ this.sendFile();
+ }, () => {
+ this.isFileUploading = !!this.addedFiles.filter(v => v.status === 'uploading').length;
+ this.sendFile();
+ });
+ } else {
+ this.addedFiles.splice(this.addedFiles.indexOf(file), 1);
+ this.isFileUploading = !!this.addedFiles.filter(v => v.status === 'uploading').length;
+ this.sendFile();
+ }
+
+ }
+ private uploadNewFile(file) {
+ const path = file.path.indexOf('/') !== -1 ? this.path.slice(this.path.indexOf('/') + 1) : '';
+ const fullPath = path ? `${path}/${file.name}` : file.name;
const formData = new FormData();
- formData.append('file', this.uploadForm.get('file').value);
- formData.append('object', path);
+ formData.append('size', file.file.size);
+ formData.append('object', fullPath);
formData.append('bucket', this.bucketName);
formData.append('endpoint', this.endpoint);
- this.isUploading = true;
- this.bucketBrowserService.uploadFile(formData)
- .subscribe(() => {
- this.bucketDataService.refreshBucketdata(this.data.bucket, this.data.endpoint);
- this.addedFiles = [];
- this.isUploading = false;
- this.toastr.success('File successfully uploaded!', 'Success!');
- }, error => this.toastr.error(error.message || 'File uploading error!', 'Oops!')
- );
+ formData.append('file', file.file);
+ file.status = 'waiting';
+ file.request = this.bucketBrowserService.uploadFile(formData);
+ this.sendFile(file);
+ }
+
+ public sendFile(file?) {
+ const waitUploading = this.addedFiles.filter(v => v.status === 'waiting');
+ const uploading = this.addedFiles.filter(v => v.status === 'uploading');
+ this.isQueueFull = !!waitUploading.length;
+ this.isFileUploading = !!this.addedFiles.filter(v => v.status === 'uploading').length;
+ // console.log((this.getTokenValidTime() / 1000 / 60 ).toFixed(0) + ' minutes');
+ if ((this.refreshTokenLimit > this.getTokenValidTime()) && !this.isTokenRefreshing) {
+ this.refreshToken();
+ }
+ if (waitUploading.length && uploading.length < 10) {
+ if (!file) {
+ file = waitUploading[0];
+ }
+ file.status = 'uploading';
+ this.isFileUploading = !!this.addedFiles.filter(v => v.status === 'uploading').length;
+ this.isQueueFull = !!this.addedFiles.filter(v => v.status === 'waiting').length;
+ file.subscr = file.request.subscribe((event: any) => {
+ if (event.type === HttpEventType.UploadProgress) {
+ file.progress = Math.round(95 * event.loaded / event.total);
+ if (file.progress === 95 && !file.interval) {
+ file.interval = setInterval(() => {
+ if (file.progress < 99) {
+ return file.progress++;
+ }
+ }, file.size < 1094967296 ? 12000 : 20000);
+ }
+ } else if (event['type'] === HttpEventType.Response) {
+ window.clearInterval(file.interval);
+ file.status = 'uploaded';
+ delete file.request;
+ this.sendFile(this.addedFiles.filter(v => v.status === 'waiting')[0]);
+ this.bucketDataService.refreshBucketdata(this.bucketName, this.data.endpoint);
+ }
+ }, error => {
+ window.clearInterval(file.interval);
+ file.status = 'failed';
+ delete file.request;
+ this.sendFile(this.addedFiles.filter(v => v.status === 'waiting')[0]);
+ }
+ );
+ }
+
+ }
+
+ public refreshBucket() {
+ this.path = '';
+ this.bucketDataService.refreshBucketdata(this.bucketName, this.data.endpoint);
+ this.isSelectionOpened = false;
+ }
+
+ public openBucket($event) {
+ this.bucketName = $event.name;
+ this.path = '';
+ this.bucketDataService.refreshBucketdata(this.bucketName, $event.endpoint);
+ this.isSelectionOpened = false;
+ }
+
+ public createFolder(folder) {
+ this.allDisable = true;
+ this.folderTreeComponent.addNewItem(folder, '', false);
}
public fileAction(action) {
- this.selected = this.folderItems.filter(item => item.isSelected);
- const selected = this.folderItems.filter(item => item.isSelected)[0];
- const path = encodeURIComponent(`${this.pathInsideBucket}${this.selected[0].item}`);
+ const selected = this.folderItems.filter(item => item.isSelected);
+ const folderSelected = this.folderItems.filter(item => item.isFolderSelected);
if (action === 'download') {
- selected['isDownloading'] = true;
+ this.clearSelection();
+ this.isActionsOpen = false;
+ const path = encodeURIComponent(`${this.pathInsideBucket}${selected[0].item}`);
+ selected[0]['isDownloading'] = true;
+ this.folderItems.forEach(item => item.isSelected = false);
this.bucketBrowserService.downloadFile(`/${this.bucketName}/object/${path}/endpoint/${this.endpoint}/download`)
- .subscribe(data => {
- FileUtils.downloadBigFiles(data, selected.item);
- selected['isDownloading'] = false;
- this.folderItems.forEach(item => item.isSelected = false);
- }, error => this.toastr.error(error.message || 'File downloading error!', 'Oops!')
- );
+ .subscribe(event => {
+ if (event['type'] === HttpEventType.DownloadProgress) {
+ selected[0].progress = Math.round(100 * event['loaded'] / selected[0].object.size);
+ }
+ if (event['type'] === HttpEventType.Response) {
+ FileUtils.downloadBigFiles(event['body'], selected[0].item);
+ setTimeout(() => {
+ selected[0]['isDownloading'] = false;
+ selected[0].progress = 0;
+ }, 1000);
+ }
+ }, error => {
+ this.toastr.error(error.message || 'File downloading error!', 'Oops!');
+ selected[0]['isDownloading'] = false;
+ }
+ );
}
if (action === 'delete') {
- this.bucketBrowserService.deleteFile(`/${this.bucketName}/object/${path}/endpoint/${this.endpoint}`).subscribe(() => {
+ const itemsForDeleting = [...folderSelected, ...selected];
+ const objects = itemsForDeleting.map(obj => obj.object.object);
+ const dataForServer = [];
+ objects.forEach(object => {
+ dataForServer.push(...this.bucketDataService.serverData.map(v => v.object).filter(v => v.indexOf(object) === 0));
+ });
+
+ this.dialog.open(BucketConfirmationDialogComponent, {data: {items: itemsForDeleting, type: 'delete'} , width: '550px'})
+ .afterClosed().subscribe((res) => {
+ !res && this.clearSelection();
+ res && this.bucketBrowserService.deleteFile({
+ bucket: this.bucketName, endpoint: this.endpoint, 'objects': dataForServer
+ }).subscribe(() => {
+ this.bucketDataService.refreshBucketdata(this.bucketName, this.data.endpoint);
+ this.toastr.success('Objects successfully deleted!', 'Success!');
+ this.clearSelection();
+ }, error => {
+ this.toastr.error(error.message || 'Objects deleting error!', 'Oops!');
+ this.clearSelection();
+ });
+ });
- this.bucketDataService.refreshBucketdata(this.data.bucket, this.data.endpoint);
- this.toastr.success('File successfully deleted!', 'Success!');
- this.folderItems.forEach(item => item.isSelected = false);
- }, error => this.toastr.error(error.message || 'File deleting error!', 'Oops!')
- );
}
}
+
+ public toogleActions() {
+ this.isActionsOpen = !this.isActionsOpen;
+ }
+
+ public closeActions() {
+ this.isActionsOpen = false;
+ }
+
+ public copyPath() {
+ const selected = this.folderItems.filter(item => item.isSelected || item.isFolderSelected)[0];
+ CopyPathUtils.copyPath(selected.object.bucket + '/' + selected.object.object);
+ this.clearSelection();
+ this.isActionsOpen = false;
+ this.toastr.success('Object path successfully copied!', 'Success!');
+ }
+
+ public toggleBucketSelection() {
+ this.isSelectionOpened = !this.isSelectionOpened;
+ }
+
+ public closeFilterInput() {
+ this.isFilterVisible = false;
+ this.searchValue = '';
+ this.filterObjects();
+ }
+
+ public convertDate(date) {
+ const utcDate = new Date(date);
+ return new Date(utcDate.setTime( utcDate.getTime() - utcDate.getTimezoneOffset() * 60 * 1000 ));
+ }
+
+ // public toggleFileUploaded($event: any) {
+ // this.isFileUploaded = $event;
+ // }
}
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/bucket-browser/bucket-browser.module.ts
similarity index 56%
copy from services/self-service/src/main/resources/webapp/src/app/resources/resources.module.ts
copy to services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-browser.module.ts
index c87d12c..2654704 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/bucket-browser/bucket-browser.module.ts
@@ -21,17 +21,17 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
-import { MaterialModule } from '../shared/material.module';
-import { ResourcesComponent } from './resources.component';
-import { ResourcesGridModule } from './resources-grid';
-import { ExploratoryEnvironmentCreateModule } from './exploratory/create-environment';
-import { ManageUngitComponent } from './manage-ungit/manage-ungit.component';
-import { ConfirmDeleteAccountDialog } from './manage-ungit/manage-ungit.component';
-import {BucketBrowserComponent} from './bucket-browser/bucket-browser.component';
-import {FolderTreeComponent} from './bucket-browser/folder-tree/folder-tree.component';
-import {MatTreeModule} from '@angular/material/tree';
-import {BucketDataService} from './bucket-browser/bucket-data.service';
+import { MaterialModule } from '../../shared/material.module';
+import { ResourcesGridModule } from '../resources-grid';
+import { ExploratoryEnvironmentCreateModule } from '../exploratory/create-environment';
+import {BucketBrowserComponent} from './bucket-browser.component';
+import {FolderTreeComponent} from './folder-tree/folder-tree.component';
+import {MatTreeModule} from '@angular/material/tree';
+import {BucketDataService} from './bucket-data.service';
+import {BucketConfirmationDialogComponent} from './bucket-confirmation-dialog/bucket-confirmation-dialog.component';
+import {BucketTreeComponent} from './buckets-tree/bucket-tree.component';
+import {ConvertFileSizePipeModule} from '../../core/pipes/convert-file-size';
@NgModule({
imports: [
@@ -41,17 +41,16 @@ import {BucketDataService} from './bucket-browser/bucket-data.service';
ResourcesGridModule,
ExploratoryEnvironmentCreateModule,
MaterialModule,
- MatTreeModule
+ MatTreeModule,
+ ConvertFileSizePipeModule
],
declarations: [
- ResourcesComponent,
- ManageUngitComponent,
- ConfirmDeleteAccountDialog,
BucketBrowserComponent,
- FolderTreeComponent
+ FolderTreeComponent,
+ BucketTreeComponent,
+ BucketConfirmationDialogComponent
],
- entryComponents: [ManageUngitComponent, ConfirmDeleteAccountDialog, BucketBrowserComponent, FolderTreeComponent],
+ entryComponents: [BucketBrowserComponent, FolderTreeComponent, BucketTreeComponent, BucketConfirmationDialogComponent],
providers: [BucketDataService],
- exports: [ResourcesComponent]
})
-export class ResourcesModule { }
+export class BucketBrowserModule { }
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-confirmation-dialog/bucket-confirmation-dialog.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-confirmation-dialog/bucket-confirmation-dialog.component.html
new file mode 100644
index 0000000..642e1e6
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-confirmation-dialog/bucket-confirmation-dialog.component.html
@@ -0,0 +1,122 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you under the Apache License, Version 2.0 (the
+ ~ "License"); you may not use this file except in compliance
+ ~ with the License. You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing,
+ ~ software distributed under the License is distributed on an
+ ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ ~ KIND, either express or implied. See the License for the
+ ~ specific language governing permissions and limitations
+ ~ under the License.
+ -->
+
+<div id="dialog-box" class="confirmation-dialog">
+
+ <header class="dialog-header">
+ <h4 class="modal-title">
+ <span *ngIf="data.type === 'delete'">Delete</span>
+ <span *ngIf="data.type === 'upload-dublicat'">Resolve conflicts</span>
+ <span *ngIf="data.type === 'cancel-uploading'">Cancel</span>
+ <span *ngIf="data.type === 'uploading-error'">Upload limitation</span>
+ </h4>
+ <button type="button" class="close" (click)="dialogRef.close()">×</button>
+ </header>
+
+ <div class="dialog-content">
+ <div class="content-box">
+ <div *ngIf="data.type === 'delete'">
+ <mat-list class="resources">
+
+ <mat-list-item class="list-header">
+ <div class="object">Object</div>
+ <div class="size">Size</div>
+ </mat-list-item>
+
+ <div class="scrolling-content delete-list" id="scrolling">
+
+ <mat-list-item *ngFor="let object of data.items" class="delete-item">
+ <div class="object">
+ <span *ngIf="object['children']"><i class="material-icons folder-icon" >folder</i></span>
+ <span *ngIf="!object['children']"><i class="material-icons folder-icon file-icon" >description</i></span>
+ <div class="ellipsis"
+ matTooltip="{{object['item']}}"
+ matTooltipPosition="above"
+ matTooltipShowDelay="1000"
+ [matTooltipClass]="'bucket-item-tooltip'">{{object['item']}}</div>
+ </div>
+ <div class="size">{{object['children'] ? '-' : object['object'].size | convertFileSize}}</div>
+ </mat-list-item>
+
+ </div>
+ </mat-list>
+
+ <div mat-dialog-content class="bottom-message" *ngIf="data.type === 'delete'">
+ <span class="confirm-message" *ngIf="isFolders">All affected objects will be deleted.</span>
+ <span class="confirm-message" *ngIf="!isFolders"><span *ngIf="data.items.length > 1">These objects</span><span *ngIf="data.items.length === 1">This object</span> will be deleted.</span>
+ </div>
+
+ <div class="text-center m-top-20">
+ <p class="strong">Do you want to proceed?</p>
+ </div>
+ </div>
+
+ <div *ngIf="data.type === 'upload-dublicat'">
+ <p>
+ <span
+ class="strong upload-item-name ellipsis"
+ matTooltip="{{data.items}}"
+ matTooltipPosition="above"
+ matTooltipShowDelay="1000"
+ [matTooltipClass]="'bucket-item-tooltip'"
+ >{{data.items}}</span> already exists in selected folder. How would you like to resolve this conflict?</p>
+ <mat-radio-group
+ aria-labelledby="upload-radio-group-label"
+ class="upload-radio-group"
+ [(ngModel)]="fileAction">
+ <mat-radio-button class="upload-radio-button" *ngFor="let action of uploadActions" [value]="action">
+ {{action}}
+ </mat-radio-button>
+ </mat-radio-group>
+ <div class="repeat-for-all" >
+ <div class="empty-checkbox" [ngClass]="{'checked': actionForAll}" (click)="toggleActionForAll();$event.stopPropagation()" >
+ <span class="checked-checkbox" *ngIf="actionForAll"></span>
+ </div>
+ <span class="repeat-message" (click)="toggleActionForAll();$event.stopPropagation()">Repeat for all remaining conflicts</span>
+ </div>
+ </div>
+
+ <div *ngIf="data.type === 'cancel-uploading'">
+ <p class="upload-message"><span>Cancel uploading file </span> <span class="strong ellipsis upload-item-name">{{data.items.name}}.</span></p>
+ <div class="text-center m-top-20">
+ <span class="strong">Do you want to proceed?</span>
+ </div>
+ </div>
+
+ <div *ngIf="data.type === 'uploading-error'">
+ <p class="upload-limit-message" *ngIf="data.items.toMany">Only the first fifty objects will be uploaded.</p>
+ <p class="upload-limit-message" *ngIf="data.items.toBig">Only file(s) within 4 GB will be uploaded.</p>
+ <div class="text-center m-top-20">
+ <span class="strong">Do you want to proceed?</span>
+ </div>
+ </div>
+
+ <div class="text-center m-top-20" *ngIf="data.type === 'delete' || data.type === 'cancel-uploading' || data.type === 'uploading-error'">
+ <button mat-raised-button type="button" class="butt action" (click)="dialogRef.close()">No</button>
+ <button mat-raised-button type="button" class="butt butt-success action" (click)="dialogRef.close(true)">Yes</button>
+ </div>
+
+ <div class="text-center m-top-20" *ngIf="data.type === 'upload-dublicat'">
+ <button mat-raised-button type="button" class="butt action" (click)="dialogRef.close()">Cancel</button>
+ <button mat-raised-button type="button" class="butt butt-success action" (click)="submitResolving()">Continue</button>
+ </div>
+
+ </div>
+ </div>
+</div>
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-confirmation-dialog/bucket-confirmation-dialog.component.scss b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-confirmation-dialog/bucket-confirmation-dialog.component.scss
new file mode 100644
index 0000000..4d7ab0c
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-confirmation-dialog/bucket-confirmation-dialog.component.scss
@@ -0,0 +1,176 @@
+/*!
+ * 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.
+ */
+
+.confirmation-dialog {
+
+ .folder-icon{
+ color: rgb(232, 232, 232);
+ margin-right: 3px;
+ transform: translateX(-2px);
+ &.file-icon{
+ transform: translateX(-4px);
+ margin-right: -2px;
+ }
+ }
+ h3{
+ margin-bottom: 20px;
+ }
+ color: #718ba6;
+ p {
+ font-size: 14px;
+ font-weight: 400;
+ margin: 0;
+ margin-bottom: 10px;
+ &.upload-limit-message{
+ text-align: center;
+ color: red;
+ font-size: 14px;
+ }
+ &.info {
+ font-weight: 500;
+ }
+ }
+ .resources {
+ .mat-list-base .mat-list-item.delete-item{
+ height: 30px;
+ }
+
+ .object {
+ width: 70%;
+ display: flex;
+ align-items: center;
+ padding-right: 10px;
+ }
+
+ .size {
+ width: 30%;
+ }
+ .scrolling-content.delete-list {
+ max-height: 200px;
+ overflow-y: auto;
+ padding-top: 11px;
+ }
+ }
+ .empty-list {
+ display: flex;
+ width: 100%;
+ justify-content: center;
+ color: #35afd5;
+ padding: 15px;
+ }
+
+ .list-header {
+ border-top: 1px solid #edf1f5;
+ border-bottom: 1px solid #edf1f5;
+ color: #577289;
+ width: 100%;
+ }
+
+ .bottom-message{
+ padding-top: 15px;
+ text-align: center;
+ .confirm-message{
+ color: #ef5c4b;
+ font-size: 13px;
+ min-height: 18px;
+ text-align: center;
+ padding-top: 20px}
+ }
+.upload-item-name{
+ max-width: 300px;
+ display: inline-block;
+ margin-bottom: -4px;
+}
+
+.mat-radio-button{
+ font-family: inherit;
+ .mat-radio-ripple{
+ display: none;
+ }
+}
+ .upload-radio-group{
+ display: flex;
+ flex-direction: column;
+ .upload-radio-button {
+ padding: 10px 0;
+
+ .mat-radio-container {
+ width: 15px;
+ height: 15px;
+ .mat-radio-outer-circle, .mat-radio-inner-circle {
+ width: 15px;
+ height: 15px;
+ border-color: lightgrey;
+ }
+ }
+
+ &.mat-radio-checked {
+ .mat-radio-outer-circle {
+ border-color: #35afd5;
+ }
+
+ .mat-radio-inner-circle {
+ background-color: #35afd5;
+ }
+ }
+ }
+ }
+ .repeat-for-all{
+ display: flex;
+ margin-top: 15px;
+ margin-bottom: 40px;
+ .repeat-message{
+ padding-left: 5px;
+ cursor: pointer;
+ }
+ }
+ .empty-checkbox {
+ min-width: 16px;
+ width: 16px;
+ height: 16px;
+ border-radius: 2px;
+ border: 2px solid lightgrey;
+ margin-top: 2px;
+ position: relative;
+ cursor: pointer;
+ &.checked {
+ border-color: #35afd5;
+ background-color: #35afd5;
+ }
+ .checked-checkbox {
+ top: 0;
+ left: 4px;
+ width: 5px;
+ height: 10px;
+ border-bottom: 2px solid white;
+ border-right: 2px solid white;
+ position: absolute;
+ transform: rotate(45deg);
+ }
+ }
+}
+.upload-message{
+ display: flex;
+ .upload-item-name{
+ max-width: 340px;
+ display: block;
+ padding-left: 4px;
+ }
+}
+
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-confirmation-dialog/bucket-confirmation-dialog.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-confirmation-dialog/bucket-confirmation-dialog.component.ts
new file mode 100644
index 0000000..2d75b20
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-confirmation-dialog/bucket-confirmation-dialog.component.ts
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Component, OnInit, Inject, Input, Output, EventEmitter, ViewEncapsulation } from '@angular/core';
+import { ToastrService } from 'ngx-toastr';
+import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+
+@Component({
+ selector: 'bucket-confirmation-dialog',
+ templateUrl: 'bucket-confirmation-dialog.component.html',
+ styleUrls: ['./bucket-confirmation-dialog.component.scss'],
+ encapsulation: ViewEncapsulation.None
+})
+
+export class BucketConfirmationDialogComponent implements OnInit {
+ isFolders: boolean = false;
+ uploadActions = ['Replace existing object', 'Skip uploading object'];
+ fileAction: string = this.uploadActions[1];
+ actionForAll: boolean = false;
+ constructor(
+ @Inject(MAT_DIALOG_DATA) public data: any,
+ public dialogRef: MatDialogRef<BucketConfirmationDialogComponent>,
+ public toastr: ToastrService
+ ) {
+
+ }
+
+ ngOnInit() {
+ if (this.data.type === 'delete') {
+ this.isFolders = !!this.data.items.filter(v => v.children).length;
+ }
+ }
+
+ toggleActionForAll() {
+ this.actionForAll = !this.actionForAll;
+ }
+
+ submitResolving() {
+ const submitObj = {replaceObject: !this.uploadActions.indexOf(this.fileAction), forAll: this.actionForAll};
+ this.dialogRef.close(submitObj);
+ }
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-data.service.ts b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-data.service.ts
index f87c575..0ca004e 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-data.service.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/bucket-data.service.ts
@@ -21,14 +21,17 @@ import { Injectable } from '@angular/core';
import { BehaviorSubject} from 'rxjs';
import {BucketBrowserService, TodoItemNode} from '../../core/services/bucket-browser.service';
-const array = [{'bucket': 'ofuks-1304-prj1-local-bucket', 'object': '4.txt', 'size': '18 bytes', 'creationDate': '21-4-2020 11:36:36'}, {'bucket': 'ofuks-1304-prj1-local-bucket', 'object': '5.txt', 'size': '18 bytes', 'creationDate': '21-4-2020 11:56:46'}, {'bucket': 'ofuks-1304-prj1-local-bucket', 'object': 'Untitled', 'size': '5 bytes', 'creationDate': '13-4-2020 03:39:11'}, {'bucket': 'ofuks-1304-prj1-local-bucket', 'object': 'adasdas', 'size': '1 KB', 'creationDate': '15-4-2020 02:17 [...]
+const array = [{'bucket': 'ofuks-1304-prj1-local-bucket', 'object': 'folder11/', 'size': '18 bytes', 'lastModifiedDate': '21-4-2020 11:36:36'}, {'bucket': 'ofuks-1304-prj1-local-bucket', 'object': 'folder11/dsafaraorueajkegrgavhsfnvgahsfgsdjfhagsdjfg497frgfhsdajfsgdafjsxzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzdvcfdvbgffgsdfgdsafaraorueajkegrgavhsfnvgahsfgsdjfhagsdjfg497frgfhsdajfsgdafjsxzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzdvcfdvbgffgsdfgdsfgsdfgggggg', ' [...]
+{'bucket': 'ofuks-1304-prj1-local-bucket', 'object': '3.jpg', 'size': '5 bytes', 'lastModifiedDate': '14-4-2020 09:36:16'}, {'bucket': 'ofuks-1304-prj1-local-bucket', 'object': '2.jpg', 'size': '52 bytes', 'lastModifiedDate': '17-4-2020 12:13:26'}, {'bucket': 'ofuks-1304-prj1-local-bucket', 'object': '1test', 'size': '112 bytes', 'lastModifiedDate': '14-4-2020 04:55:02'}, {'bucket': 'ofuks-1304-prj1-local-bucket', 'object': 'test.pem', 'size': '1 KB', 'lastModifiedDate': '14-4-2020 04:57 [...]
+{'bucket': 'ofuks-1304-prj1-local-bucket', 'object': '5.jpg', 'size': '52 bytes', 'lastModifiedDate': '17-4-2020 12:13:26'}, {'bucket': 'ofuks-1304-prj1-local-bucket', 'object': 'test', 'size': '12 bytes', 'lastModifiedDate': '14-4-2020 04:55:02'}, {'bucket': 'ofuks-1304-prj1-local-bucket', 'object': 'test.pem', 'size': '1 KB', 'lastModifiedDate': '14-4-2020 04:57:54'}, {'bucket': 'ofuks-1304-prj1-local-bucket', 'object': 'test1', 'size': '52 bytes', 'lastModifiedDate': '17-4-2020 11:22: [...]
@Injectable()
export class BucketDataService {
public _bucketData = new BehaviorSubject<any>(null);
- private serverData: any = [];
+ public serverData: any = [];
get data(): TodoItemNode[] { return this._bucketData.value; }
+ emptyFolder = null;
constructor(
private bucketBrowserService: BucketBrowserService,
) {
@@ -37,43 +40,63 @@ export class BucketDataService {
public refreshBucketdata(bucket, endpoint) {
let backetData = [];
this.bucketBrowserService.getBucketData(bucket, endpoint).subscribe(v => {
+ const copiedData = JSON.parse(JSON.stringify(v));
+
this.serverData = v;
- backetData = this.convertToFolderTree(v);
+ if (this.emptyFolder) {
+ copiedData.unshift(this.emptyFolder);
+ }
+ backetData = this.convertToFolderTree(copiedData);
const data = this.buildFileTree({[bucket]: backetData}, 0);
this._bucketData.next(data);
});
-
- // this.serverData = array;
- // backetData = this.convertToFolderTree(array);
- // const data = this.buildFileTree({[bucket]: backetData}, 0);
- // this._bucketData.next(data);
- // }
+ // this.serverData = array;
+ // backetData = this.convertToFolderTree(array);
+ // const data = this.buildFileTree({[bucket]: backetData}, 0);
+ // this._bucketData.next(data);
}
+
public buildFileTree(obj: {[key: string]: any}, level: number): TodoItemNode[] {
return Object.keys(obj).reduce<TodoItemNode[]>((accumulator, key) => {
+ if (key === '') {
+ return accumulator;
+ }
const value = obj[key];
const node = new TodoItemNode();
node.item = key;
- if (Object.keys(value).length) {
+ if (value === '') {
+ node.children = this.buildFileTree({}, level + 1);
+ return accumulator.concat(node);
+ }
+ if (Object.keys(value).filter(v => v !== 'obj').length > 0) {
if (typeof value === 'object') {
+ node.object = value.obj || {'bucket': node.item, 'object': '', 'size': '', 'lastModifiedDate': ''};
+ delete value.obj;
node.children = this.buildFileTree(value, level + 1);
} else {
node.item = value;
}
} else {
- node.size = this.serverData.filter(v => v.object.indexOf(node.item) !== -1)[0];
+ node.object = value.obj;
}
return accumulator.concat(node);
}, []);
}
- public insertItem(parent: TodoItemNode, name, isFile) {
- if (parent.children) {
+ public insertItem(parent: TodoItemNode, name, isFile, emptyFolderObj?) {
+ if (parent.children) {
if (isFile) {
- parent.children.push(name as TodoItemNode);
+ parent.children.unshift(name as TodoItemNode);
} else {
- parent.children.unshift({item: name, children: []} as TodoItemNode);
- this._bucketData.next(this.data);
+ if (name) {
+ const child = {item: name, children: [], object: JSON.parse(JSON.stringify(parent.object))};
+ child.object.object = child.object.object.slice(0, -1) + child.item + '/';
+ parent.children.unshift(child as TodoItemNode);
+ } else {
+ parent.children.unshift({item: '', children: [], object: {}} as TodoItemNode);
+ this.emptyFolder = emptyFolderObj;
+ this._bucketData.next(this.data);
+ }
}
}
}
@@ -83,26 +106,36 @@ export class BucketDataService {
this._bucketData.next(this.data);
}
- public processFiles = (files, target) => {
+ public removeItem(parent, child) {
+ parent.children.splice( parent.children.indexOf(child), 1);
+ this._bucketData.next(this.data);
+ }
+
+ public processFiles = (files, target, object) => {
let pointer = target;
files.forEach((file) => {
if (!pointer[file]) {
pointer[file] = {};
}
pointer = pointer[file];
+ if (!pointer.obj) {
+ pointer.obj = object;
+ }
});
- }
+ };
public processFolderArray = (acc, curr) => {
- const files = curr.object.split('/').filter(x => x.length > 0);
- this.processFiles(files, acc);
- return acc;
- }
+ const files = curr.object.split('/');
+ this.processFiles(files, acc, curr);
- public convertToFolderTree = (data) => data
- .reduce(
- this.processFolderArray,
- {}
- )
+ return acc;
+ };
+ public convertToFolderTree = (data) => {
+ const finalData = data.reduce(this.processFolderArray, {});
+ if (Object.keys(finalData).length === 0) {
+ return '';
+ }
+ return finalData;
+ }
}
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/buckets-tree/bucket-tree.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/buckets-tree/bucket-tree.component.html
new file mode 100644
index 0000000..8d294a5
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/buckets-tree/bucket-tree.component.html
@@ -0,0 +1,25 @@
+<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
+ <mat-tree-node
+ *matTreeNodeDef="let node"
+ matTreeNodePadding matTreeNodePaddingIndent="17"
+ (click)="openBucketData(node)"
+ [ngClass]="{'active-item': activeBacket === node}">
+ <button mat-icon-button disabled></button>
+ <div
+ class="ellipsis"
+ matTooltip="{{node.name}}"
+ matTooltipPosition="above"
+ matTooltipShowDelay="1000"
+ [matTooltipClass]="'bucket-item-tooltip'"
+ >{{node.name}}</div>
+ </mat-tree-node>
+ <mat-tree-node *matTreeNodeDef="let node;when: hasChild" matTreeNodePadding matTreeNodePaddingIndent="17">
+ <button mat-icon-button matTreeNodeToggle
+ [attr.aria-label]="'toggle ' + node.name">
+ <mat-icon class="mat-icon-rtl-mirror">
+ {{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
+ </mat-icon>
+ </button>
+ <span (click)="toggleProject(node, treeControl.isExpanded(node))">{{node.name}}</span>
+ </mat-tree-node>
+</mat-tree>
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/buckets-tree/bucket-tree.component.scss b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/buckets-tree/bucket-tree.component.scss
new file mode 100644
index 0000000..fd06d7a
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/buckets-tree/bucket-tree.component.scss
@@ -0,0 +1,106 @@
+
+.folder{
+ padding-left: 5px;
+ max-width: 350px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+
+}
+
+.mat-tree{
+ font-family: 'Open Sans', sans-serif;
+}
+
+.folder-icon{
+ color: rgb(232, 232, 232);
+}
+
+.folder-item-line{
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+
+}
+
+.active-item {
+ color: #00bcd4;
+ i{
+ color: #00bcd4;
+ }
+
+
+}
+
+.add-folder-form{
+ display: flex;
+ align-items: center;
+}
+
+.mat-tree-node:not(.input-node){
+ cursor: pointer;
+ transition: .3s;
+ overflow: unset;
+ min-height: auto;
+ button.mat-icon-button{
+ width: 25px;
+ height: 25px;
+ line-height: 28px;
+ }
+ &:hover{
+ color: #00bcd4;
+ i{
+ color: #00bcd4;
+ }
+ }
+}
+
+.input-node {
+ overflow: unset;
+ padding-top: 5px;
+ padding-bottom: 7px;
+
+
+ button.mat-icon-button {
+ &.action-btn {
+ color: lightgrey;
+ font-size: 20px;
+ cursor: not-allowed;
+ }
+
+ &.check {
+ color: #49af38;
+ cursor: pointer;
+
+ &:hover {
+ border-color: #49af38;
+ background: #f9fafb;
+ }
+ }
+
+ &.close {
+ color: #f1696e;
+ cursor: pointer;
+
+ &:hover {
+ border-color: #f1696e;
+ background: #f9fafb;
+ }
+ }
+ }
+
+ .mat-error {
+ background-color: #ffffff;
+ }
+
+ .d-none{
+ display: none;
+ }
+
+}
+
+
+
+
+
+
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/buckets-tree/bucket-tree.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/buckets-tree/bucket-tree.component.ts
new file mode 100644
index 0000000..ed222f6
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/buckets-tree/bucket-tree.component.ts
@@ -0,0 +1,101 @@
+import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
+import {FlatTreeControl} from '@angular/cdk/tree';
+import {MatTreeFlatDataSource, MatTreeFlattener} from '@angular/material/tree';
+
+
+interface BucketNode {
+ name: string;
+ endpoint?: string;
+ children?: BucketNode[];
+}
+
+/** Flat node with expandable and level information */
+interface BucketFlatNode {
+ expandable: boolean;
+ name: string;
+ level: number;
+}
+
+@Component({
+ selector: 'dlab-bucket-tree',
+ templateUrl: './bucket-tree.component.html',
+ styleUrls: ['./bucket-tree.component.scss']
+})
+
+export class BucketTreeComponent implements OnInit {
+ @Output() emitActiveBucket: EventEmitter<{}> = new EventEmitter();
+ @Input() openedBucket: string;
+ @Input() buckets: BucketNode[];
+
+ private _transformer = (node: BucketNode, level: number) => {
+ return {
+ expandable: !!node.children && node.children.length > 0,
+ name: node.name,
+ endpoint: node.endpoint,
+ level: level,
+ };
+ }
+
+ treeControl = new FlatTreeControl<BucketFlatNode>(
+ node => node.level, node => node.expandable);
+
+ treeFlattener = new MatTreeFlattener(
+ this._transformer, node => node.level, node => node.expandable, node => node.children);
+
+ dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
+ private activeBucketName: string;
+ private activeBacket: any;
+
+ constructor() {
+ }
+
+ ngOnInit() {
+ this.activeBucketName = this.openedBucket || '';
+ this.dataSource.data = this.buckets;
+ this.setActiveBucket();
+ }
+
+ public openBucketData(bucket) {
+ this.dataSource['_treeControl'].collapseAll();
+ this.setActiveBucket(bucket);
+ this.emitActiveBucket.emit(bucket);
+ }
+
+ public setActiveBucket(bucket?) {
+ this.activeBacket = bucket || this.dataSource._flattenedData.getValue().filter(v => v.name === this.openedBucket)[0];
+ this.expandAllParents(this.activeBacket);
+ }
+
+ public toggleProject(el, isExpanded){
+ isExpanded ? this.treeControl.collapse(el) : this.treeControl.expand(el);
+ }
+
+ private expandAllParents(el) {
+ if (el) {
+ this.treeControl.expand(el);
+ if (this.getParentNode(el) !== null) {
+ this.expandAllParents(this.getParentNode(el));
+ }
+ }
+ }
+
+ private getParentNode(node: BucketFlatNode): BucketFlatNode | null {
+ const currentLevel = node.level;
+ if (currentLevel < 1) {
+ return null;
+ }
+
+ const startIndex = this.treeControl.dataNodes.indexOf(node) - 1;
+
+ for (let i = startIndex; i >= 0; i--) {
+ const currentNode = this.treeControl.dataNodes[i];
+
+ if (currentNode.level < currentLevel) {
+ return currentNode;
+ }
+ }
+ return null;
+ }
+
+ public hasChild = (_: number, node: BucketFlatNode) => node.expandable;
+}
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.html
index b663e78..dab723d 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.html
@@ -1,32 +1,65 @@
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
- <mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle matTreeNodePadding [ngStyle]="{'display': 'none'}">
- <button mat-icon-button disabled></button>
- <!-- <mat-checkbox class="checklist-leaf-node"-->
- <!-- [checked]="checklistSelection.isSelected(node)"-->
- <!-- (change)="todoLeafItemSelectionToggle(node)"></mat-checkbox>-->
- {{node.item}}
-</mat-tree-node>
- <mat-tree-node *matTreeNodeDef="let node; when: hasNoContent" matTreeNodePadding>
+ <mat-tree-node
+ *matTreeNodeDef="let node"
+ matTreeNodeToggle matTreeNodePadding
+ matTreeNodePaddingIndent="17"
+ [ngStyle]="{'display': 'none'}"
+ >
<button mat-icon-button disabled></button>
- <mat-form-field>
- <mat-label>New folder</mat-label>
- <input matInput #itemValue>
- </mat-form-field>
- <button mat-button (click)="saveNode(node, itemValue.value)">Save</button>
+ {{node.item}}
</mat-tree-node>
- <mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
+ <mat-tree-node *matTreeNodeDef="let node; when: hasNoContent" matTreeNodePadding matTreeNodePaddingIndent="17" class="input-node">
+ <form class="add-folder-form" id="folder-form">
+ <mat-form-field>
+ <mat-label>New folder</mat-label>
+ <input matInput #itemValue [formControl]="folderFormControl" [errorStateMatcher]="matcher">
+ <mat-error *ngIf="!folderFormControl.hasError('required') && !folderFormControl.hasError('isDuplicate')">
+ The folder name can only contain Latin letters, numbers and special characters except for #, ?, /, \, "
+ </mat-error>
+ <mat-error *ngIf="folderFormControl.hasError('required')">
+ Folder name is <strong>required</strong>
+ </mat-error>
+ <mat-error *ngIf="folderFormControl.hasError('isDuplicate')">
+ Folder with this name already exists
+ </mat-error>
+ </mat-form-field>
+ <button (click)="saveNode(node, itemValue.value)"
+ [ngClass]="{'check': folderFormControl.valid && folderFormControl.dirty && !folderCreating}"
+ mat-icon-button class="btn action-btn"
+ [disabled]="!folderFormControl.valid || !folderFormControl.dirty"
+ matTooltip="Please wait! Folder is creating."
+ [matTooltipDisabled]="!folderCreating"
+ matTooltipPosition="above"
+ >
+ <span><i class="material-icons ">check</i></span></button>
+ <button (click)="removeItem(node)" mat-icon-button class="btn close action-btn"><span ><i class="material-icons ">close</i></span></button>
+ </form>
+ </mat-tree-node>
+
+ <mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding matTreeNodePaddingIndent="17" [ngClass]="{'cursor-not-allow': bucketDataService.emptyFolder}">
<button mat-icon-button matTreeNodeToggle
- [attr.aria-label]="'toggle ' + node.filename">
- <mat-icon class="mat-icon-rtl-mirror" [ngClass]="{'active-item': selectedFolder === node}">
+ [attr.aria-label]="'toggle ' + node.filename" [ngClass]="{'not-allowed': bucketDataService.emptyFolder}">
+ <mat-icon class="mat-icon-rtl-mirror" [ngClass]="{'active-item': (selectedFolder === node && !bucketDataService.emptyFolder)}">
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
</mat-icon>
</button>
-<!-- <mat-checkbox [checked]="descendantsAllSelected(node)"-->
-<!-- [indeterminate]="descendantsPartiallySelected(node)"-->
-<!-- (change)="todoItemSelectionToggle(node)"></mat-checkbox>-->
- <div (click)="showItem(node)" class="folder-item-line" [ngClass]="{'active-item': selectedFolder === node}"><i class="material-icons folder-icon">folder</i> <span class="folder">{{node.item}}</span></div>
-<!-- <button mat-icon-button (click)="addNewItem(node)"><mat-icon>add</mat-icon></button>-->
+ <div
+ (click)="showItem(node)"
+ class="folder-item-line"
+ [ngClass]="{'active-item': (selectedFolder === node && !bucketDataService.emptyFolder), 'not-allowed': bucketDataService.emptyFolder}"
+ >
+ <i class="material-icons folder-icon">folder</i>
+ <span
+ class="folder"
+ matTooltip="{{node.item}}"
+ matTooltipPosition="above"
+ matTooltipShowDelay="1000"
+ [matTooltipClass]="'bucket-item-tooltip'"
+ >
+ {{node.item}}
+ </span>
+ </div>
</mat-tree-node>
</mat-tree>
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.scss b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.scss
index e9902af..22c36d0 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.scss
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.scss
@@ -1,6 +1,14 @@
.folder{
padding-left: 5px;
+ max-width: 350px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+.mat-tree{
+ font-family: 'Open Sans', sans-serif;
}
.folder-icon{
@@ -15,7 +23,6 @@
}
.active-item {
- //border-bottom: 1px solid #00bcd4;
color: #00bcd4;
i{
color: #00bcd4;
@@ -24,10 +31,24 @@
}
-.mat-tree-node{
+.add-folder-form{
+ display: flex;
+ align-items: center;
+}
+
+.mat-tree-node:not(.input-node){
cursor: pointer;
transition: .3s;
overflow: unset;
+ min-height: auto;
+ button.mat-icon-button{
+ width: 25px;
+ height: 25px;
+ line-height: 28px;
+ }
+}
+
+.mat-tree-node:not(.input-node):not(.cursor-not-allow){
&:hover{
color: #00bcd4;
i{
@@ -36,3 +57,47 @@
}
}
+.input-node {
+ overflow: unset;
+ padding-top: 5px;
+ padding-bottom: 7px;
+
+
+ button.mat-icon-button {
+ &.action-btn {
+ color: lightgrey;
+ font-size: 20px;
+ cursor: not-allowed;
+ }
+
+ &.check {
+ color: #49af38;
+ cursor: pointer;
+
+ &:hover {
+ border-color: #49af38;
+ background: #f9fafb;
+ }
+ }
+
+ &.close {
+ color: #f1696e;
+ cursor: pointer;
+
+ &:hover {
+ border-color: #f1696e;
+ background: #f9fafb;
+ }
+ }
+ }
+
+ .mat-error {
+ background-color: #ffffff;
+ }
+}
+
+
+
+
+
+
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.ts
index 179a4c1..fa40d1e 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/bucket-browser/folder-tree/folder-tree.component.ts
@@ -1,10 +1,21 @@
-import {Component, OnInit, AfterViewInit, Output, EventEmitter, OnDestroy} from '@angular/core';
-import {SelectionModel} from '@angular/cdk/collections';
+import {Component, Output, EventEmitter, OnDestroy, Input, OnInit} from '@angular/core';
import {FlatTreeControl} from '@angular/cdk/tree';
import {MatTreeFlatDataSource, MatTreeFlattener} from '@angular/material/tree';
import {BucketBrowserService, TodoItemFlatNode, TodoItemNode} from '../../../core/services/bucket-browser.service';
import {BucketDataService} from '../bucket-data.service';
import {Subscription} from 'rxjs';
+import {FormControl, FormGroupDirective, NgForm, Validators} from '@angular/forms';
+import {ErrorStateMatcher} from '@angular/material/core';
+import {PATTERNS} from '../../../core/util';
+import {ToastrService} from 'ngx-toastr';
+import {HttpResponse} from '@angular/common/http';
+
+export class MyErrorStateMatcher implements ErrorStateMatcher {
+ isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
+ const isSubmitted = form && form.submitted;
+ return !!(control && control.invalid && (control.dirty));
+ }
+}
@@ -14,44 +25,57 @@ import {Subscription} from 'rxjs';
styleUrls: ['./folder-tree.component.scss']
})
-export class FolderTreeComponent implements OnInit, OnDestroy {
+export class FolderTreeComponent implements OnDestroy {
@Output() showFolderContent: EventEmitter<any> = new EventEmitter();
+ @Output() disableAll: EventEmitter<any> = new EventEmitter();
+ @Input() folders;
+ @Input() endpoint;
+
private folderTreeSubs;
private path = [];
private selectedFolder: TodoItemFlatNode;
private flatNodeMap = new Map<TodoItemFlatNode, TodoItemNode>();
private nestedNodeMap = new Map<TodoItemNode, TodoItemFlatNode>();
- private selectedParent: TodoItemFlatNode | null = null;
- private newItemName = '';
+
+ private folderCreating = false;
private subscriptions: Subscription = new Subscription();
public treeControl: FlatTreeControl<TodoItemFlatNode>;
private treeFlattener: MatTreeFlattener<TodoItemNode, TodoItemFlatNode>;
public dataSource: MatTreeFlatDataSource<TodoItemNode, TodoItemFlatNode>;
- private checklistSelection = new SelectionModel<TodoItemFlatNode>(true /* multiple */);
-
constructor(
+ public toastr: ToastrService,
private bucketBrowserService: BucketBrowserService,
private bucketDataService: BucketDataService,
- ) {
+ ) {
this.treeFlattener = new MatTreeFlattener(this.transformer, this.getLevel, this.isExpandable, this.getChildren);
this.treeControl = new FlatTreeControl<TodoItemFlatNode>(this.getLevel, this.isExpandable);
this.dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
-
- this.subscriptions.add(bucketDataService._bucketData.subscribe(data => {
- if (data) {
- this.dataSource.data = data;
+ this.subscriptions.add(this.bucketDataService._bucketData.subscribe(data => {
+ if (data) {
+ this.dataSource.data = data;
const subject = this.dataSource._flattenedData;
- this.folderTreeSubs = subject.subscribe((subjectData) => {
+ const subjectData = subject.getValue();
if (this.selectedFolder) {
this.selectedFolder = subjectData.filter(v => v.item === this.selectedFolder.item && v.level === this.selectedFolder.level)[0];
}
this.expandAllParents(this.selectedFolder || subjectData[0]);
this.showItem(this.selectedFolder || subjectData[0]);
- });
+ if (this.selectedFolder && !this.bucketDataService.emptyFolder) {
+ setTimeout(() => {
+ const element = document.querySelector('.folder-item-line.active-item');
+ element && element.scrollIntoView({ block: 'center', behavior: 'smooth' });
+ }, 0);
+ } else if (this.selectedFolder && this.bucketDataService.emptyFolder) {
+ setTimeout(() => {
+ const element = document.querySelector('#folder-form');
+ element && element.scrollIntoView({ block: 'end', behavior: 'smooth' });
+ }, 0);
+ }
}
}));
+ this.dataSource._flattenedData.subscribe();
}
getLevel = (node: TodoItemFlatNode) => node.level;
@@ -62,7 +86,7 @@ export class FolderTreeComponent implements OnInit, OnDestroy {
hasChild = (_: number, _nodeData: TodoItemFlatNode) => _nodeData.expandable;
- hasNoContent = (_: number, _nodeData: TodoItemFlatNode) => _nodeData.item === '';
+ hasNoContent = (_: number, _nodeData: TodoItemFlatNode) => _nodeData.item === '' || _nodeData.item === 'ا';
transformer = (node: TodoItemNode, level: number) => {
const existingNode = this.nestedNodeMap.get(node);
@@ -77,12 +101,25 @@ export class FolderTreeComponent implements OnInit, OnDestroy {
return flatNode;
}
-
- ngOnInit() {
- }
-
ngOnDestroy() {
this.bucketDataService._bucketData.next([]);
+ this.subscriptions.unsubscribe();
+ this.bucketDataService.emptyFolder = null;
+ }
+
+ folderFormControl = new FormControl('', [
+ Validators.required,
+ Validators.pattern(PATTERNS.folderRegex),
+ this.duplicate.bind(this)
+ ]);
+
+ matcher = new MyErrorStateMatcher();
+
+ private duplicate(control) {
+ if (control && control.value) {
+ const isDublicat = this.folders.slice(1).some(folder => folder.item === control.value);
+ return isDublicat ? { isDuplicate: true } : null;
+ }
}
private showItem(el) {
@@ -94,7 +131,8 @@ export class FolderTreeComponent implements OnInit, OnDestroy {
const data = {
flatNode: el,
element: this.flatNodeMap.get(el),
- path: path.join('/')
+ path: path.map(v => v.item).join('/'),
+ pathObject: path
};
this.showFolderContent.emit(data);
}
@@ -103,10 +141,10 @@ export class FolderTreeComponent implements OnInit, OnDestroy {
private getPath(el) {
if (el) {
if (this.path.length === 0) {
- this.path.unshift(el.item);
+ this.path.unshift(el);
}
if (this.getParentNode(el) !== null) {
- this.path.unshift(this.getParentNode(el).item);
+ this.path.unshift(this.getParentNode(el));
this.getPath(this.getParentNode(el));
}
return this.path;
@@ -122,63 +160,6 @@ export class FolderTreeComponent implements OnInit, OnDestroy {
}
}
- private descendantsAllSelected(node: TodoItemFlatNode): boolean {
-
- const descendants = this.treeControl.getDescendants(node);
- const descAllSelected = descendants.every(child =>
- this.checklistSelection.isSelected(child)
- );
- return descAllSelected;
- }
-
- private descendantsPartiallySelected(node: TodoItemFlatNode): boolean {
-
- const descendants = this.treeControl.getDescendants(node);
- const result = descendants.some(child => this.checklistSelection.isSelected(child));
- return result && !this.descendantsAllSelected(node);
- }
-
- private todoItemSelectionToggle(node: TodoItemFlatNode): void {
- this.checklistSelection.toggle(node);
- const descendants = this.treeControl.getDescendants(node);
- this.checklistSelection.isSelected(node)
-? this.checklistSelection.select(...descendants)
- : this.checklistSelection.deselect(...descendants);
-
- // Force update for the parent
- descendants.every(child =>
- this.checklistSelection.isSelected(child)
-);
- this.checkAllParentsSelection(node);
-}
-
- private todoLeafItemSelectionToggle(node: TodoItemFlatNode): void {
- this.checklistSelection.toggle(node);
- this.checkAllParentsSelection(node);
- }
-
- private checkAllParentsSelection(node: TodoItemFlatNode): void {
- let parent: TodoItemFlatNode | null = this.getParentNode(node);
- while (parent) {
- this.checkRootNodeSelection(parent);
- parent = this.getParentNode(parent);
- }
- }
-
-
- private checkRootNodeSelection(node: TodoItemFlatNode): void {
- const nodeSelected = this.checklistSelection.isSelected(node);
- const descendants = this.treeControl.getDescendants(node);
- const descAllSelected = descendants.every(child =>
- this.checklistSelection.isSelected(child)
- );
- if (nodeSelected && !descAllSelected) {
- this.checklistSelection.deselect(node);
- } else if (!nodeSelected && descAllSelected) {
- this.checklistSelection.select(node);
- }
- }
-
private getParentNode(node: TodoItemFlatNode): TodoItemFlatNode | null {
const currentLevel = this.getLevel(node);
if (currentLevel < 1) {
@@ -197,15 +178,67 @@ export class FolderTreeComponent implements OnInit, OnDestroy {
return null;
}
- private addNewItem(node: TodoItemFlatNode, file, isFile, path) {
- const parentNode = this.flatNodeMap.get(node);
- this.bucketDataService.insertItem(parentNode!, file, isFile);
- this.treeControl.expand(node);
+
+private addNewItem(node: TodoItemFlatNode, file, isFile) {
+ const currNode = this.flatNodeMap.get(node);
+ if (!currNode.object) {
+ currNode.object = {bucket: currNode.item, object: ''};
+ }
+ const emptyFolderObject = currNode.object;
+ if (emptyFolderObject.object.lastIndexOf('ا') !== emptyFolderObject.object.length - 1 || emptyFolderObject.object === '') {
+ emptyFolderObject.object += 'ا';
+ }
+ this.bucketDataService.insertItem(currNode!, file, isFile, emptyFolderObject);
+ this.treeControl.expand(node);
+ setTimeout(() => {
+ const element = document.querySelector('#folder-form');
+ element && element.scrollIntoView({ block: 'end', behavior: 'smooth' });
+ }, 0);
+ }
+
+ private removeItem(node: TodoItemFlatNode) {
+ const parentNode = this.flatNodeMap.get(this.getParentNode(node));
+ const childNode = this.flatNodeMap.get(node);
+ this.bucketDataService.emptyFolder = null;
+ this.bucketDataService.removeItem(parentNode!, childNode);
+ this.resetForm();
}
private saveNode(node: TodoItemFlatNode, itemValue: string) {
- const nestedNode = this.flatNodeMap.get(node);
- this.bucketDataService.updateItem(nestedNode!, itemValue);
+ this.folderCreating = true;
+ const parent = this.getParentNode(node);
+ const flatParent = this.flatNodeMap.get(parent);
+ let flatObject = flatParent.object.object;
+ if (flatObject.indexOf('ا') === flatObject.length - 1) {
+ flatObject = flatObject.substring(0, flatParent.object.object.length - 1);
+ }
+ const path = `${ flatParent.object && flatObject !== '/' ? flatObject : ''}${itemValue}/`;
+ const bucket = flatParent.object ? flatParent.object.bucket : flatParent.item;
+ const formData = new FormData();
+ formData.append('file', '');
+ formData.append('object', path);
+ formData.append('bucket', bucket);
+ formData.append('endpoint', this.endpoint);
+ this.bucketDataService.emptyFolder = null;
+ this.bucketBrowserService.uploadFile(formData)
+ .subscribe((event) => {
+ if (event instanceof HttpResponse) {
+ this.bucketDataService.insertItem(flatParent, itemValue, false);
+ // this.bucketDataService.refreshBucketdata(bucket, this.endpoint);
+ this.toastr.success('Folder successfully created!', 'Success!');
+ this.folderCreating = false;
+ this.removeItem(node);
+ }
+ }, error => {
+ this.folderCreating = false;
+ this.toastr.error(error.message || 'Folder creation error!', 'Oops!');
+ });
+ }
+ private resetForm() {
+ this.folderFormControl.setValue('');
+ this.folderFormControl.updateValueAndValidity();
+ this.folderFormControl.markAsPristine();
+ this.disableAll.emit(false);
}
}
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/computational/cluster-details/cluster-details.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/computational/cluster-details/cluster-details.component.html
index 04ea086..44718bd 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/computational/cluster-details/cluster-details.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/computational/cluster-details/cluster-details.component.html
@@ -100,7 +100,7 @@
<div class="m-top-10">
<p *ngFor="let item of resource.computational_url" class="ellipsis flex">
<span class="strong">{{ item.description }}:</span>
- <a href="{{item.url}}" target="_blank" matTooltip="{{item.url}}"
+ <a (click)="logAction(resource.computational_name, item.description)" href="{{item.url}}" target="_blank" matTooltip="{{item.url}}"
matTooltipPosition="above">{{ item.url }}</a>
</p>
</div>
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/computational/cluster-details/cluster-details.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/computational/cluster-details/cluster-details.component.ts
index 11002e1..415cfaa 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/computational/cluster-details/cluster-details.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/computational/cluster-details/cluster-details.component.ts
@@ -26,6 +26,7 @@ import { DateUtils, CheckUtils } from '../../../core/util';
import { DataengineConfigurationService } from '../../../core/services';
import { DICTIONARY } from '../../../../dictionary/global.dictionary';
import { CLUSTER_CONFIGURATION } from '../computational-resource-create-dialog/cluster-configuration-templates';
+import {AuditService} from '../../../core/services/audit.service';
@Component({
selector: 'dlab-cluster-details',
@@ -52,13 +53,12 @@ export class DetailComputationalResourcesComponent implements OnInit {
public toastr: ToastrService,
public dialogRef: MatDialogRef<DetailComputationalResourcesComponent>,
private dataengineConfigurationService: DataengineConfigurationService,
- private _fb: FormBuilder
+ private _fb: FormBuilder,
+ private auditService: AuditService
) { }
ngOnInit() {
this.open(this.data.environment, this.data.resource);
-
- console.log(this.PROVIDER);
}
public open(environment, resource): void {
@@ -117,4 +117,9 @@ export class DetailComputationalResourcesComponent implements OnInit {
? (control.value && control.value !== null && CheckUtils.isJSON(control.value) ? null : { valid: false })
: null;
}
+
+ private logAction(name: any, description: string) {
+ this.auditService.sendDataToAudit({resource_name: name, info: ['User opened link' + description]}).subscribe();
+ console.log(`${name}: ${description}`);
+ }
}
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/cost-details-dialog/cost-details-dialog.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/cost-details-dialog/cost-details-dialog.component.html
index 1331f5f..17fe71a 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/cost-details-dialog/cost-details-dialog.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/cost-details-dialog/cost-details-dialog.component.html
@@ -50,8 +50,8 @@
</div>
<div class="service">{{ item.product }}</div>
<!-- <div class="resource-type" >{{ item.resourse_type }}</div>-->
- <div class="usage-date-start">{{ item.from | date }}</div>
- <div class="usage-date-end">{{ item.to | date }}</div>
+ <div class="usage-date-start">{{ item.from.join('/') | date }}</div>
+ <div class="usage-date-end">{{ item.to.join('/') | date }}</div>
<div class="cost-currency">{{ item.cost }} {{ item.currency }}</div>
</mat-list-item>
</div>
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.html
index cd39c46..2095926 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.html
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.html
@@ -22,10 +22,10 @@
<button type="button" class="close" (click)="dialogRef.close()">×</button>
</header>
<div class="dialog-content">
- <div *ngIf="data">
+ <div *ngIf="data ">
<table class="detail-header">
<tr>
- <td>{{notebook.template_name}}</td>
+ <td>{{notebook.template_name || notebook.name}}</td>
<td>
<span class="status" ngClass="{{notebook.status || ''}}">
{{notebook.status}}
@@ -34,21 +34,33 @@
<td>{{notebook.shape}}</td>
</tr>
</table>
+
<div class="content-box">
<div class="detail-info" *ngIf="notebook.error_message">
<p class="failed">{{ notebook.error_message }}</p>
</div>
-
- <div class="scroll-box" id="scrolling">
+ <div *ngIf="data.type === 'environment'" class="detail-info">
+ <p>Open following URL(s) in your browser to access this box:</p>
+ <div class="links_block">
+ <p *ngFor="let item of notebook.exploratory_urls">
+ <span class="description">{{item.description}}: </span>
+ <a class="ellipsis" matTooltip="{{item.url}}" matTooltipPosition="above" href="{{item.url}}"
+ target="_blank" (click)="logAction(notebook.name, item.description)">
+ {{item.url}}{{notebook.name}}
+ </a>
+ </p>
+ </div>
+ </div>
+ <div class="scroll-box" id="scrolling" *ngIf="data.type === 'resource'">
<div class="detail-info" *ngIf="!notebook.error_message">
<p>Edge Node IP Address {{notebook.node_ip}}</p>
<p *ngIf="notebook.status === 'running'">Up time {{upTimeInHours}} hour(s) since
{{upTimeSince || "not specified."}}</p>
- <p>Open following URL(s) in your browser to access this box:</p>
+ <p *ngIf="notebook.url?.length">Open following URL(s) in your browser to access this box:</p>
<div class="links_block">
<p *ngFor="let item of notebook.url">
<span class="description">{{item.description}}: </span>
- <a class="ellipsis" matTooltip="{{item.url}}" matTooltipPosition="above" href="{{item.url}}"
+ <a (click)="logAction(notebook.name, item.description)" class="ellipsis" matTooltip="{{item.url}}" matTooltipPosition="above" href="{{item.url}}"
target="_blank">
{{item.url}}
</a>
@@ -59,42 +71,105 @@
<p class="flex" *ngIf="notebook.password">Password: <span
class="strong">{{ notebook.password }}</span></p>
- <p class="m-top-30">{{ DICTIONARY[PROVIDER].personal_storage }}: </p>
- <div class="links_block"
- (click)="bucketBrowser(notebook.bucket_name, notebook.endpoint, this.bucketStatus.view)"
- [matTooltip]="'You have not permission to open project bucket'"
- matTooltipDisabled="{{this.bucketStatus.view}}"
- matTooltipPosition="above"
- [ngClass]="{'not-allow': !this.bucketStatus.view}"
- >
- <p *ngIf="DICTIONARY[PROVIDER].cloud_provider === 'azure' && notebook.account_name">{{ DICTIONARY[PROVIDER].account }}
- <span class="bucket-info" [ngClass]="{'not-allow': !this.bucketStatus.view}">{{ notebook.account_name}}</span></p>
- <p *ngIf="notebook.bucket_name">{{ DICTIONARY[PROVIDER].container }} <span
- class="bucket-info" [ngClass]="{'not-allow': !this.bucketStatus.view}">{{ notebook.bucket_name }}</span></p>
- </div>
- <p>Shared endpoint bucket: </p>
- <div class="links_block" (click)="bucketBrowser(notebook.shared_bucket_name, notebook.endpoint, this.bucketStatus.view)"
- [matTooltip]="'You have not permission to open shared endpoint bucket'"
- matTooltipDisabled="{{this.bucketStatus.view}}"
- matTooltipPosition="above"
- [ngClass]="{'not-allow': !this.bucketStatus.view}"
- >
- <p *ngIf="DICTIONARY[PROVIDER].cloud_provider === 'azure' && notebook.shared_account_name">{{ DICTIONARY[PROVIDER].account }}
- <span class="bucket-info" [ngClass]="{'not-allow': !this.bucketStatus.view}">{{ notebook.shared_account_name}}</span></p>
- <p *ngIf="notebook.shared_bucket_name">{{ DICTIONARY[PROVIDER].container }}
- <span class="bucket-info" [ngClass]="{'not-allow': !this.bucketStatus.view}">{{ notebook.shared_bucket_name }}</span></p>
- </div>
- <br />
+ <p class="m-top-30">{{ 'Project bucket' }}: </p>
+ <!-- (click)="bucketBrowser(notebook.bucket_name, notebook.endpoint, this.bucketStatus.view)"-->
+ <div class="links_block" (mouseleave)="hideCopyIcon()">
+ <p *ngIf="PROVIDER === 'azure' && notebook.account_name">
+ <span
+ class="bucket-info"
+ (mouseover)="showCopyIcon()"
+ [matTooltip]="notebook.bucket_name + '@' + notebook.account_name + '.blob.core.windows.net'"
+ matTooltipPosition="above"
+ [matTooltipClass]="'bucket-item-tooltip'"
+ >
+ {{notebook.bucket_name + '@' + notebook.account_name + '.blob.core.windows.net'}}
+<!-- rc-22-projecta-conteiner@1ebobsvx7t.blob.core.windows.net-->
+ </span>
- <div *ngIf="DICTIONARY[PROVIDER].cloud_provider === 'azure' && notebook.datalake_name">
- <p>Data Lake Store: </p>
- <div class="links_block">
- <p>Data Lake Store Account: <span class="bucket-info">{{ notebook.datalake_name }}</span></p>
- <p>Personal folder: <span class="bucket-info">{{ notebook.datalake_directory }}</span></p>
- <p>Collaboration folder: <span class="bucket-info">{{ notebook.datalake_shared_directory }}</span>
+ <span *ngIf="isCopyIconVissible" [matTooltip]="isCopied ? 'Copy bucket name' : 'Copied'" matTooltipPosition="above">
+ <span class="link-icon" (click)="copyBucketName(notebook.bucket_name + '@' + notebook.account_name + '.blob.core.windows.net');$event.stopPropagation()" >
+ <span _ngcontent-xpv-c19="" class="material-icons" (click)="this.isCopied = false">content_copy</span>
+ </span>
+ </span>
+ </p>
+ <p *ngIf="notebook.bucket_name && PROVIDER !== 'azure'">{{ DICTIONARY[PROVIDER].container }}
+ <span
+ class="bucket-info"
+ (mouseover)="showCopyIcon()"
+ >
+ {{ notebook.bucket_name }}
+ </span>
+ <span *ngIf="isCopyIconVissible" [matTooltip]="isCopied ? 'Copy bucket name' : 'Copied'" matTooltipPosition="above">
+ <span class="link-icon" (click)="copyBucketName(notebook.bucket_name);$event.stopPropagation()" >
+ <span _ngcontent-xpv-c19="" class="material-icons" (click)="this.isCopied = false">content_copy</span>
+ </span>
+ </span>
</p>
- </div>
</div>
+ <div class="bucket-info bucket-link">
+ <span></span>
+<!-- <button-->
+<!-- type="button"-->
+<!-- class="butt"-->
+<!-- mat-raised-button-->
+<!-- >-->
+<!-- Open bucket browser-->
+<!-- </button>-->
+<!-- <span class="description open-bucket"-->
+<!-- [ngClass]="{'not-allow': !this.bucketStatus.view || !thisdata.buckets.length}"-->
+<!-- (click)="bucketBrowser(notebook.bucket_name, notebook.endpoint, this.bucketStatus.view && thisdata.buckets.length)"-->
+<!-- >-->
+ <span class="description open-bucket"
+ [matTooltip]="!this.bucketStatus.view
+ ? 'You have not permission to open bucket'
+ : 'You have not any bucket'"
+ matTooltipDisabled="{{this.bucketStatus.view && this.data.buckets.length}}"
+ matTooltipPosition="above"
+ [matTooltipClass]="'bucket-item-tooltip'"
+ [ngClass]="{'not-allow': !this.bucketStatus.view || !this.data.buckets.length}"
+ (click)="bucketBrowser(notebook.bucket_name, notebook.endpoint, this.bucketStatus.view && this.data.buckets.length)"
+ >
+ Open bucket browser
+ </span>
+ </div>
+<!-- <p>Shared endpoint bucket: </p>-->
+<!-- <div class="links_block" (click)="bucketBrowser(notebook.shared_bucket_name, notebook.endpoint, this.bucketStatus.view)"-->
+<!-- [matTooltip]="'You have not permission to open bucket'"-->
+<!-- matTooltipDisabled="{{this.bucketStatus.view}}"-->
+<!-- matTooltipPosition="above"-->
+<!-- [matTooltipClass]="'bucket-item-tooltip'"-->
+<!-- >-->
+<!-- <p *ngIf="DICTIONARY[PROVIDER === 'azure' && notebook.shared_account_name">{{ DICTIONARY[PROVIDER].account }}-->
+<!-- <span class="bucket-info bucket-link" [ngClass]="{'not-allow': !this.bucketStatus.view}" (mouseover)="showCopyIcon('shared')">{{ notebook.shared_account_name}}</span>-->
+<!-- <span *ngIf="isCopyIconVissible.shared" class="link-icon" (click)="copyBucketName(notebook.shared_account_name)">-->
+<!-- <span _ngcontent-xpv-c19="" class="material-icons" matTooltip="Copy bucket name" matTooltipPosition="above">content_copy</span>-->
+<!-- </span>-->
+<!-- </p>-->
+<!-- <p *ngIf="notebook.shared_bucket_name">{{ DICTIONARY[PROVIDER].container }}-->
+<!-- <span-->
+<!-- class="bucket-info bucket-link"-->
+<!-- [ngClass]="{'not-allow': !this.bucketStatus.view}"-->
+<!-- (mouseover)="showCopyIcon('shared')"-->
+<!-- (click)="bucketBrowser(notebook.shared_bucket_name, notebook.endpoint, this.bucketStatus.view)"-->
+<!-- >-->
+<!-- {{ notebook.shared_bucket_name }}-->
+<!-- </span>-->
+<!-- <span *ngIf="isCopyIconVissible.shared" class="link-icon" (click)="copyBucketName(notebook.shared_bucket_name)">-->
+<!-- <span _ngcontent-xpv-c19="" class="material-icons" matTooltip="Copy bucket name" matTooltipPosition="above">content_copy</span>-->
+<!-- </span>-->
+<!-- </p>-->
+<!-- </div>-->
+<!-- <br />-->
+
+<!-- <div *ngIf="DICTIONARY[PROVIDER === 'azure' && notebook.datalake_name">-->
+<!-- <p>Data Lake Store: </p>-->
+<!-- <div class="links_block">-->
+<!-- <p>Data Lake Store Account: <span class="bucket-info">{{ notebook.datalake_name }}</span></p>-->
+<!-- <p>Personal folder: <span class="bucket-info">{{ notebook.datalake_directory }}</span></p>-->
+<!-- <p>Collaboration folder: <span class="bucket-info">{{ notebook.datalake_shared_directory }}</span>-->
+<!-- </p>-->
+<!-- </div>-->
+<!-- </div>-->
<!-- <p>
<a href="#/help/accessnotebookguide" target="_blank">
@@ -140,5 +215,6 @@
</div>
</div>
</div>
+
</div>
</div>
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.scss b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.scss
index 24d273f..c6722ef 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.scss
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.scss
@@ -73,7 +73,6 @@
}
span {
-
.danger_color {
position: absolute;
bottom: -16px;
@@ -84,11 +83,23 @@
.bucket-info {
padding-left: 7px;
+ margin-right: 10px;
font-weight: 600;
color: $blue-grey-color;
- cursor: pointer;
}
-.not-allow{
- cursor: not-allowed;
+.bucket-link{
+ padding: 15px;
+ padding-left: 0;
+ color: #35afd5;
+ .open-bucket{
+ cursor: pointer;
+ font-size: 14px;
+ }
+ &.not-allow{
+ cursor: not-allowed;
+ color: $blue-grey-color !important;
+ }
}
+
+
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.ts
index ed359c8..b2788f5 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/detail-dialog/detail-dialog.component.ts
@@ -27,6 +27,8 @@ import { DICTIONARY } from '../../../../dictionary/global.dictionary';
import { DataengineConfigurationService } from '../../../core/services';
import { CLUSTER_CONFIGURATION } from '../../computational/computational-resource-create-dialog/cluster-configuration-templates';
import {BucketBrowserComponent} from '../../bucket-browser/bucket-browser.component';
+import {CopyPathUtils} from '../../../core/util/copyPathUtils';
+import {AuditService} from '../../../core/services/audit.service';
@Component({
selector: 'detail-dialog',
@@ -36,18 +38,22 @@ import {BucketBrowserComponent} from '../../bucket-browser/bucket-browser.compon
export class DetailDialogComponent implements OnInit {
readonly DICTIONARY = DICTIONARY;
- readonly PROVIDER = this.data.notebook.cloud_provider;
+ readonly PROVIDER = this.data.notebook.cloud_provider.toLowerCase();
+ private isCopied: boolean = true;
notebook: any;
upTimeInHours: number;
upTimeSince: string = '';
tooltip: boolean = false;
config: Array<{}> = [];
bucketStatus: object = {};
+ isBucketAllowed = true;
+ isCopyIconVissible = false;
public configurationForm: FormGroup;
@ViewChild('configurationNode', { static: false }) configuration;
+
constructor(
@Inject(MAT_DIALOG_DATA) public data: any,
private dataengineConfigurationService: DataengineConfigurationService,
@@ -55,6 +61,7 @@ export class DetailDialogComponent implements OnInit {
public dialogRef: MatDialogRef<DetailDialogComponent>,
private dialog: MatDialog,
public toastr: ToastrService,
+ public auditService: AuditService
) {
}
@@ -69,6 +76,11 @@ export class DetailDialogComponent implements OnInit {
this.upTimeSince = (this.notebook.time) ? new Date(this.notebook.time).toString() : '';
this.initFormModel();
this.getClusterConfiguration();
+ if (this.notebook.edgeNodeStatus === 'terminated' ||
+ this.notebook.edgeNodeStatus === 'terminating' ||
+ this.notebook.edgeNodeStatus === 'failed') {
+ this.isBucketAllowed = false;
+ }
}
}
@@ -125,9 +137,31 @@ export class DetailDialogComponent implements OnInit {
}
public bucketBrowser(bucketName, endpoint, permition): void {
- permition && this.dialog.open(BucketBrowserComponent, { data:
- {bucket: bucketName, endpoint: endpoint, bucketStatus: this.bucketStatus},
+ if (!permition) {
+ return;
+ }
+ bucketName = this.isBucketAllowed ? this.notebook.bucket_name : this.data.buckets[0].children[0].name;
+ // bucketName = 'ofuks-1304-pr2-local-bucket';
+ this.dialog.open(BucketBrowserComponent, { data:
+ {bucket: bucketName, endpoint: endpoint, bucketStatus: this.bucketStatus, buckets: this.data.buckets},
panelClass: 'modal-fullscreen' })
.afterClosed().subscribe();
}
+
+ protected showCopyIcon() {
+ this.isCopyIconVissible = true;
+ }
+ protected hideCopyIcon() {
+ this.isCopyIconVissible = false;
+ this.isCopied = true;
+ }
+
+ protected copyBucketName(copyValue) {
+ CopyPathUtils.copyPath(copyValue);
+ }
+
+ private logAction(name: any, description: string) {
+ this.auditService.sendDataToAudit({resource_name: name, info: ['User opened link' + description]}).subscribe();
+ console.log(`${name}: ${description}`);
+ }
}
diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/install-libraries/install-libraries.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/install-libraries/install-libraries.component.ts
index 2f2d733..0445b4c 100644
--- a/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/install-libraries/install-libraries.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/resources/exploratory/install-libraries/install-libraries.component.ts
@@ -255,6 +255,7 @@ export class InstallLibrariesComponent implements OnInit, OnDestroy {
lib.version = 'v.' + lib.version;
}
);
+ this.filterLibs();
this.changeDetector.markForCheck();
this.filterConfiguration.group = this.createFilterList(this.notebookLibs.map(v => this.groupsListMap[v.group]));
this.filterConfiguration.resource = this.createFilterList(this.notebookLibs.map(lib => lib.status.map(status => status.resource)));
@@ -363,12 +364,17 @@ export class InstallLibrariesComponent implements OnInit, OnDestroy {
<h4 class="modal-title">Library installation error</h4>
<button type="button" class="close" (click)="dialogRef.close()">×</button>
</div>
- <div class="content">{{ data }}</div>
+ <div class="content lib-error" >
+ {{ data }}
+ </div>
<div class="text-center">
<button type="button" class="butt" mat-raised-button (click)="dialogRef.close()">Close</button>
</div>
`,
- styles: []
+ styles: [ `
+ .lib-error { max-height: 200px; overflow-x: auto; word-break: break-all; padding: 20px 30px !important; margin: 20px 0;}
+ `
+ ]
})
export class ErrorMessageDialogComponent {
constructor(
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 738059a..70cf223 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
@@ -23,7 +23,7 @@ import { animate, state, style, transition, trigger } from '@angular/animations'
import { ToastrService } from 'ngx-toastr';
import { MatDialog } from '@angular/material/dialog';
-import { UserResourceService } from '../../core/services';
+import {ProjectService, UserResourceService} from '../../core/services';
import { ExploratoryModel, Exploratory } from './resources-grid.model';
import { FilterConfigurationModel } from './filter-configuration.model';
@@ -89,6 +89,9 @@ export class ResourcesGridComponent implements OnInit {
public displayedColumns: string[] = this.filteringColumns.map(item => item.name);
public displayedFilterColumns: string[] = this.filteringColumns.map(item => item.filter_class);
+ public bucketsList;
+ public activeProjectsList: any;
+
constructor(
@@ -96,10 +99,18 @@ export class ResourcesGridComponent implements OnInit {
private userResourceService: UserResourceService,
private dialog: MatDialog,
private progressBarService: ProgressBarService,
+ private projectService: ProjectService,
) { }
ngOnInit(): void {
this.buildGrid();
+ this.getUserProjects();
+ }
+
+ public getUserProjects() {
+ this.projectService.getUserProjectsList(true).subscribe((projects: any) => {
+ this.activeProjectsList = projects;
+ });
}
public buildGrid(): void {
@@ -107,6 +118,7 @@ export class ResourcesGridComponent implements OnInit {
this.userResourceService.getUserProvisionedResources()
.subscribe((result: any) => {
this.environments = ExploratoryModel.loadEnvironments(result);
+ this.getBuckets();
this.getDefaultFilterConfiguration();
(this.environments.length) ? this.getUserPreferences() : this.filteredEnvironments = [];
this.healthStatus && !this.healthStatus.billingEnabled && this.modifyGrid();
@@ -169,7 +181,7 @@ export class ResourcesGridComponent implements OnInit {
public printDetailEnvironmentModal(data): void {
this.dialog.open(DetailDialogComponent, { data:
- {notebook: data, bucketStatus: this.healthStatus.bucketBrowser},
+ {notebook: data, bucketStatus: this.healthStatus.bucketBrowser, buckets: this.bucketsList, type: 'resource'},
panelClass: 'modal-lg'
})
.afterClosed().subscribe(() => this.buildGrid());
@@ -330,6 +342,30 @@ export class ResourcesGridComponent implements OnInit {
if (filterConfig[index].length) this.activeFiltering = true;
}
+ public getBuckets() {
+ const bucketsList = this.environments.map(project => {
+ return Object.keys(project.projectEndpoints).map(key => {
+ if (project.endpoints.length === 0) {
+ return;
+ }
+ const currEndpoint = project.projectEndpoints[key];
+ const provider: string = project.endpoints.filter(endpoint => endpoint['name'] === key)[0]['cloudProvider'];
+ const edgeItem = {name: `${project.project} (${key})`, children: []};
+ const projectBucket = currEndpoint[this.DICTIONARY[provider.toLowerCase()].bucket_name];
+ const sharedBucket = currEndpoint[this.DICTIONARY[provider.toLowerCase()].shared_bucket_name];
+ if (projectBucket && currEndpoint.status !== 'terminated' && currEndpoint.status !== 'terminating' && currEndpoint.status !== 'failed') {
+ edgeItem.children.push({name: projectBucket, endpoint: key});
+ }
+ if (sharedBucket) {
+ edgeItem.children.push({name: sharedBucket, endpoint: key});
+ }
+ return edgeItem;
+ }).filter(v => v);
+ });
+
+ this.bucketsList = SortUtils.flatDeep(bucketsList, 1).filter(v => v.children.length);
+ }
+
private getUserPreferences(): void {
this.userResourceService.getUserPreferences()
.subscribe((result: FilterConfigurationModel) => {
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 e769dbe..c4ad5800 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
@@ -58,6 +58,8 @@ export class ExploratoryModel {
return data.map((value) => {
return {
project: value.project,
+ projectEndpoints: value.shared,
+ endpoints: value.endpoints,
exploratory: value.exploratory.map(el => {
const provider = el.cloud_provider.toLowerCase();
const billing = value.exploratoryBilling.filter(res => res.name === el.exploratory_name)[0];
@@ -101,5 +103,7 @@ 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 545412d..d47559f 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
@@ -20,11 +20,18 @@
<div class="base-retreat">
<div class="sub-nav">
<div class="selection">
+ <span
+ matTooltip="{{!healthStatus?.projectAssigned ? 'You are not assigned to any project' : 'You have not any active project'}}"
+ matTooltipPosition="above"
+ [matTooltipClass]="'bucket-item-tooltip'"
+ [matTooltipDisabled]="healthStatus?.projectAssigned && resourcesGrid.activeProjectsList?.length !== 0"
+ >
+ <span>{{resourcesGrid.activeProject}}</span>
<button mat-raised-button class="butt butt-create" (click)="createEnvironment()"
- [disabled]="!healthStatus?.projectAssigned">
+ [disabled]="!healthStatus?.projectAssigned || !resourcesGrid.activeProjectsList?.length">
<i class="material-icons">add</i>Create new
</button>
-
+ </span>
<div class="mat-reset">
<div class="control selector-wrapper" *ngIf="projects.length">
<mat-form-field>
@@ -46,9 +53,16 @@
</div>
<div>
-<!-- <button mat-raised-button class="butt butt-tool" (click)="bucketBrowser()">-->
-<!-- <i class="material-icons"></i>Bucket browser-->
-<!-- </button>-->
+ <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]="'bucket-item-tooltip'"
+ >
+ <button mat-raised-button class="butt butt-tool" (click)="bucketBrowser(this.bucketStatus?.view)"
+ [disabled]="!this.bucketStatus?.view || resourcesGrid.bucketsList?.length === 0">
+ <i class="material-icons"></i>Bucket browser
+ </button>
+ </span>
<button mat-raised-button class="butt butt-tool" (click)="manageUngit()">
<i class="material-icons"></i>Git credentials
</button>
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 7933fec..301509d 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
@@ -42,6 +42,7 @@ export class ResourcesComponent implements OnInit {
@ViewChild(ResourcesGridComponent, { static: true }) resourcesGrid: ResourcesGridComponent;
+ public bucketStatus;
constructor(
public toastr: ToastrService,
private healthStatusService: HealthStatusService,
@@ -65,6 +66,8 @@ export class ResourcesComponent implements OnInit {
this.exploratoryEnvironments = this.resourcesGrid.environments;
}
+
+
public toggleFiltering(): void {
if (this.resourcesGrid.activeFiltering) {
this.resourcesGrid.resetFilterConfigurations();
@@ -78,9 +81,17 @@ export class ResourcesComponent implements OnInit {
.afterClosed().subscribe(() => this.refreshGrid());
}
- public bucketBrowser(): void {
- this.dialog.open(BucketBrowserComponent, { panelClass: 'modal-fullscreen' })
- .afterClosed().subscribe(() => this.refreshGrid());
+ public bucketBrowser(permition): void {
+ const defaultBucket = this.resourcesGrid.bucketsList[0].children[0];
+ permition && this.dialog.open(BucketBrowserComponent, { data:
+ {
+ bucket: defaultBucket.name,
+ endpoint: defaultBucket.endpoint,
+ bucketStatus: this.bucketStatus,
+ buckets: this.resourcesGrid.bucketsList
+ },
+ panelClass: 'modal-fullscreen' })
+ .afterClosed().subscribe();
}
public setActiveProject(project): void {
@@ -106,6 +117,7 @@ export class ResourcesComponent implements OnInit {
(result: any) => {
this.healthStatus = result;
this.resourcesGrid.healthStatus = this.healthStatus;
+ this.bucketStatus = this.healthStatus.bucketBrowser;
},
error => this.toastr.error(error.message, 'Oops!'));
}
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 c87d12c..04a412e 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
@@ -27,30 +27,30 @@ import { ResourcesGridModule } from './resources-grid';
import { ExploratoryEnvironmentCreateModule } from './exploratory/create-environment';
import { ManageUngitComponent } from './manage-ungit/manage-ungit.component';
import { ConfirmDeleteAccountDialog } from './manage-ungit/manage-ungit.component';
-import {BucketBrowserComponent} from './bucket-browser/bucket-browser.component';
-import {FolderTreeComponent} from './bucket-browser/folder-tree/folder-tree.component';
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';
@NgModule({
- imports: [
- CommonModule,
- FormsModule,
- ReactiveFormsModule,
- ResourcesGridModule,
- ExploratoryEnvironmentCreateModule,
- MaterialModule,
- MatTreeModule
- ],
+ imports: [
+ CommonModule,
+ FormsModule,
+ ReactiveFormsModule,
+ ResourcesGridModule,
+ ExploratoryEnvironmentCreateModule,
+ MaterialModule,
+ MatTreeModule,
+ ConvertFileSizePipeModule,
+ BucketBrowserModule
+ ],
declarations: [
ResourcesComponent,
ManageUngitComponent,
ConfirmDeleteAccountDialog,
- BucketBrowserComponent,
- FolderTreeComponent
+
],
- entryComponents: [ManageUngitComponent, ConfirmDeleteAccountDialog, BucketBrowserComponent, FolderTreeComponent],
+ entryComponents: [ManageUngitComponent, ConfirmDeleteAccountDialog],
providers: [BucketDataService],
exports: [ResourcesComponent]
})
diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/form-controls/multi-level-select-dropdown/multi-level-select-dropdown.component.ts b/services/self-service/src/main/resources/webapp/src/app/shared/form-controls/multi-level-select-dropdown/multi-level-select-dropdown.component.ts
index e4cca34..3033acc 100644
--- a/services/self-service/src/main/resources/webapp/src/app/shared/form-controls/multi-level-select-dropdown/multi-level-select-dropdown.component.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/form-controls/multi-level-select-dropdown/multi-level-select-dropdown.component.ts
@@ -44,7 +44,7 @@ export class MultiLevelSelectDropdownComponent {
COMPUTATIONAL_SHAPE: 'Compute shapes',
NOTEBOOK_SHAPE: 'Notebook shapes',
COMPUTATIONAL: 'Compute',
- BUCKET_BROWSER: 'Bucket browser'
+ BUCKET_BROWSER: 'Bucket browser actions'
};
constructor() {
diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/material.module.ts b/services/self-service/src/main/resources/webapp/src/app/shared/material.module.ts
index c677b05..0eb5554 100644
--- a/services/self-service/src/main/resources/webapp/src/app/shared/material.module.ts
+++ b/services/self-service/src/main/resources/webapp/src/app/shared/material.module.ts
@@ -19,7 +19,6 @@
import { NgModule } from '@angular/core';
import { CdkTableModule } from '@angular/cdk/table';
-
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
@@ -51,7 +50,7 @@ import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
-import {STEPPER_GLOBAL_OPTIONS} from "@angular/cdk/stepper";
+import {STEPPER_GLOBAL_OPTIONS} from '@angular/cdk/stepper';
@NgModule({
exports: [
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 f2d715f..8f6a8ca 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
@@ -95,29 +95,37 @@
<ng-template #env><i class="material-icons">settings</i></ng-template>
</a>
</a>
- <a *ngIf="healthStatus?.billingEnabled" class="nav-item" [routerLink]="['/billing_report']"
- [routerLinkActive]="['active']" [routerLinkActiveOptions]="{exact:true}">
- <span *ngIf="isExpanded; else billing">Billing Report</span>
- <ng-template #billing><i class="material-icons">account_balance_wallet</i></ng-template>
- </a>
- </div>
- <div>
- <a class="nav-item" [routerLink]="['/swagger']" [routerLinkActive]="['active']"
- [routerLinkActiveOptions]="{exact:true}">
- <span *ngIf="isExpanded; else endpoint">Cloud Endpoint API</span>
- <ng-template #endpoint>
- <span>
- <svg width="30px" height="27px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
- <g>
- <path d="M127.059657,255.996921 C58.8506544,255.526472 -0.457073619,198.918442 0.00265506057,126.998303 C0.444649399,57.7958628 57.9516598,-0.468967577 129.11002,0.00284555012 C198.267128,0.462386081 256.613109,57.8667711 255.995136,128.194199 C256.568091,197.883453 197.934268,256.489189 127.059657,255.996921 Z M127.059657,255.996921 C58.8506544,255.526472 -0.457073619,198.918442 0.00265506057,126.998303 C0.444649399,57.7958628 57.9516598,-0.468967577 129.11002,0.0028 [...]
- <path id="swager-bgr" d="M127.184644,238.997327 C68.0323765,238.589271 16.6036091,189.498744 17.0023028,127.131428 C17.3860285,67.1185953 67.2554,16.5917106 128.963117,17.0024872 C188.934544,17.4010221 239.531905,67.1825241 238.995778,128.169251 C239.492444,188.602381 188.64743,239.424426 127.184644,238.997327 Z M127.184644,238.997327 C68.0323765,238.589271 16.6036091,189.498744 17.0023028,127.131428 C17.3860285,67.1185953 67.2554,16.5917106 128.963117,17.0024872 C188 [...]
- <path d="M169.327319,127.956161 C169.042723,133.246373 164.421106,137.639224 159.866213,136.872586 C159.844426,136.872586 159.821277,136.872586 159.798128,136.872586 C154.753021,136.879395 150.658383,132.794288 150.652936,127.749182 C150.824511,122.690458 155.019915,118.703395 160.08,118.789182 C165.125106,118.813692 169.59966,123.077182 169.327319,127.956161 Z M88.2011915,179.220161 C90.1034894,179.27599 92.0071489,179.235139 94.2008511,179.235139 L94.2008511,193.021 [...]
- </g>
- </svg>
- </span>
- </ng-template>
+ <a class="nav-item has-children" *ngIf="healthStatus?.billingEnabled || true">
+ <span *ngIf="isExpanded">Reports</span>
+ <a *ngIf="healthStatus?.billingEnabled" class="sub-nav-item" [routerLink]="['/billing_report']"
+ [routerLinkActive]="['active']" [routerLinkActiveOptions]="{exact:true}" [style.margin-left.px]="isExpanded ? '30' : '0'">
+ <span *ngIf="isExpanded; else billing">Billing</span>
+ <ng-template #billing><i class="material-icons">account_balance_wallet</i></ng-template>
+ </a>
+ <a class="sub-nav-item" [routerLink]="['/audit']" [style.margin-left.px]="isExpanded ? '30' : '0'"
+ [routerLinkActive]="['active']" [routerLinkActiveOptions]="{exact:true}">
+ <span *ngIf="isExpanded; else audit">Audit</span>
+ <ng-template #audit><i class="material-icons">library_books</i></ng-template>
+ </a>
</a>
</div>
+<!-- <div>-->
+<!-- <a class="nav-item" [routerLink]="['/swagger']" [routerLinkActive]="['active']"-->
+<!-- [routerLinkActiveOptions]="{exact:true}">-->
+<!-- <span *ngIf="isExpanded; else endpoint">Cloud Endpoint API</span>-->
+<!-- <ng-template #endpoint>-->
+<!-- <span>-->
+<!-- <svg width="30px" height="27px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">-->
+<!-- <g>-->
+<!-- <path d="M127.059657,255.996921 C58.8506544,255.526472 -0.457073619,198.918442 0.00265506057,126.998303 C0.444649399,57.7958628 57.9516598,-0.468967577 129.11002,0.00284555012 C198.267128,0.462386081 256.613109,57.8667711 255.995136,128.194199 C256.568091,197.883453 197.934268,256.489189 127.059657,255.996921 Z M127.059657,255.996921 C58.8506544,255.526472 -0.457073619,198.918442 0.00265506057,126.998303 C0.444649399,57.7958628 57.9516598,-0.468967577 129.11002,0. [...]
+<!-- <path id="swager-bgr" d="M127.184644,238.997327 C68.0323765,238.589271 16.6036091,189.498744 17.0023028,127.131428 C17.3860285,67.1185953 67.2554,16.5917106 128.963117,17.0024872 C188.934544,17.4010221 239.531905,67.1825241 238.995778,128.169251 C239.492444,188.602381 188.64743,239.424426 127.184644,238.997327 Z M127.184644,238.997327 C68.0323765,238.589271 16.6036091,189.498744 17.0023028,127.131428 C17.3860285,67.1185953 67.2554,16.5917106 128.963117,17.0024872 [...]
+<!-- <path d="M169.327319,127.956161 C169.042723,133.246373 164.421106,137.639224 159.866213,136.872586 C159.844426,136.872586 159.821277,136.872586 159.798128,136.872586 C154.753021,136.879395 150.658383,132.794288 150.652936,127.749182 C150.824511,122.690458 155.019915,118.703395 160.08,118.789182 C165.125106,118.813692 169.59966,123.077182 169.327319,127.956161 Z M88.2011915,179.220161 C90.1034894,179.27599 92.0071489,179.235139 94.2008511,179.235139 L94.2008511,193 [...]
+<!-- </g>-->
+<!-- </svg>-->
+<!-- </span>-->
+<!-- </ng-template>-->
+<!-- </a>-->
+<!-- </div>-->
</nav>
</mat-nav-list>
</mat-sidenav>
diff --git a/services/self-service/src/main/resources/webapp/src/assets/img/blank-file-svgrepo-com.svg b/services/self-service/src/main/resources/webapp/src/assets/img/blank-file-svgrepo-com.svg
new file mode 100644
index 0000000..4c88fd9
--- /dev/null
+++ b/services/self-service/src/main/resources/webapp/src/assets/img/blank-file-svgrepo-com.svg
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ viewBox="0 0 26 26" style="enable-background:new 0 0 26 26;" xml:space="preserve">
+<g>
+ <path style="fill:lightgray;" d="M20.266,4.207c-0.244-0.24-0.494-0.484-0.74-0.732c-0.248-0.246-0.492-0.496-0.732-0.74
+ C17.082,0.988,16.063,0,15,0H7C4.795,0,3,1.795,3,4v18c0,2.205,1.795,4,4,4h12c2.205,0,4-1.795,4-4V8
+ C23,6.938,22.012,5.918,20.266,4.207z M21,22c0,1.104-0.896,2-2,2H7c-1.104,0-2-0.896-2-2V4c0-1.104,0.896-2,2-2l7.289-0.004
+ C15.01,2.18,15,3.066,15,3.953V7c0,0.551,0.449,1,1,1h3c0.998,0,2,0.005,2,1V22z"/>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>
diff --git a/services/self-service/src/main/resources/webapp/src/assets/styles/_dialogs.scss b/services/self-service/src/main/resources/webapp/src/assets/styles/_dialogs.scss
index c5ac335..ccde6f7 100644
--- a/services/self-service/src/main/resources/webapp/src/assets/styles/_dialogs.scss
+++ b/services/self-service/src/main/resources/webapp/src/assets/styles/_dialogs.scss
@@ -263,6 +263,7 @@ mat-dialog-container {
p {
font-weight: 400;
margin-bottom: 10px;
+ display: flex;
small {
font-size: 12px;
@@ -275,6 +276,13 @@ mat-dialog-container {
overflow: hidden;
text-overflow: ellipsis;
display: block;
+ max-width: 450px;
+ white-space: nowrap;
+ &.link-icon{
+ //margin-left: 10px;
+ cursor: pointer;
+ padding-top: 3px;
+ }
}
.links_block {
@@ -343,6 +351,60 @@ mat-dialog-container {
overflow: auto;
}
+.bucket-browser{
+ .mat-form-field-appearance-legacy .mat-form-field-subscript-wrapper {
+ width: calc(100% + 77px);
+ }
+}
+
+.bucket-item-tooltip{
+ max-width: 1100px !important;
+ white-space: pre-line;
+}
+
+.mat-list-base .mat-list-item.delete-item{
+ height: 30px;
+}
+
+.d-none{
+ display: none !important;
+}
+
+.progres{
+ border: 1px solid rgba(0,0,0,.12);
+ height: 17px;
+ position: relative;
+ width: 155px;
+
+ .bar{
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ width: 0;
+ background-color: #00bcd4;
+ }
+
+ .progress-bar-text{
+ position: absolute;
+ left: 5px;
+ top: 0;
+ bottom: 0;
+ font-size: 11px;
+ line-height: 13px;
+ color: rgba(0, 0, 0, 0.87);
+ z-index: 10;
+ }
+}
+
+.not-allow{
+ cursor: not-allowed !important;
+ &.open-bucket{
+ color: $blue-grey-color !important;
+ }
+}
+
+
@media screen and (max-width: 1280px) {
.modal-fullscreen {
max-width: 100vw !important;
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 e1bbe94..bf22293 100644
--- a/services/self-service/src/main/resources/webapp/src/styles.scss
+++ b/services/self-service/src/main/resources/webapp/src/styles.scss
@@ -155,6 +155,10 @@ mat-chip.mat-chip strong {
pointer-events: none;
}
+.cursor-not-allow{
+ cursor: not-allowed !important;
+}
+
.not-active {
cursor: not-allowed !important;
opacity: .6;
@@ -311,12 +315,20 @@ input[type='number'] {
margin-top: 30px;
}
+.m-top-40 {
+ margin-top: 40px;
+}
+
.m-bott-10 {
margin-bottom: 10px;
}
+.m-bott-20 {
+ margin-bottom: 20px;
+}
+
.m-bott-30 {
- margin-bottom: 10px;
+ margin-bottom: 30px;
}
.m-top-10p {
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@dlab.apache.org
For additional commands, e-mail: commits-help@dlab.apache.org