This is an automated email from the ASF dual-hosted git repository. hshpak pushed a commit to branch feat/DATALAB-2781/share-one-image-with-all-project-users in repository https://gitbox.apache.org/repos/asf/incubator-datalab.git
commit 70ae3d1f8800b1955aabe2b02db9bd9864137b93 Author: viravit <[email protected]> AuthorDate: Tue Jun 7 19:15:52 2022 +0200 [DATALAB-2781] implemented logic by share image to all users --- USER_GUIDE.md | 72 +++++++++++++++------ billing_page.png | Bin 0 -> 40016 bytes doc/administration_section.png | Bin 0 -> 4551 bytes doc/audit_page.png | Bin 100615 -> 260974 bytes doc/billing_page.png | Bin 58129 -> 40016 bytes doc/bucket_button.png | Bin 75695 -> 219991 bytes doc/configuration_page.png | Bin 48255 -> 181994 bytes doc/configuration_page1.png | Bin 55341 -> 231139 bytes doc/configuration_page_prov.png | Bin 82155 -> 275773 bytes doc/configuration_page_restart.png | Bin 83666 -> 307966 bytes doc/environment_management.png | Bin 70246 -> 226589 bytes doc/image_action_menu.png | Bin 0 -> 3542 bytes doc/images_main.png | Bin 0 -> 77357 bytes doc/jupyter_kernel.png | Bin 191741 -> 130238 bytes doc/library_magic_usage.png | Bin 0 -> 11245 bytes doc/main_page.png | Bin 49382 -> 186860 bytes doc/main_page_filter.png | Bin 89155 -> 236408 bytes doc/upload_or_generate_user_key.png | Bin 51504 -> 52499 bytes doc/user.png | Bin 0 -> 1114 bytes doc/user_information.png | Bin 0 -> 9318 bytes .../src/base/scripts/create_ssh_user.py | 2 +- .../src/general/lib/os/fab.py | 31 +++++---- .../scripts/aws/common_create_notebook_image.py | 7 +- .../terraform/azure/endpoint/main/network.tf | 1 + .../service/impl/ImageExploratoryServiceImpl.java | 4 +- .../src/main/resources/webapp/package-lock.json | 32 ++++----- .../resources/webapp/src/app/app.routing.module.ts | 6 +- .../services/applicationServiceFacade.service.ts | 31 ++++++--- .../app/core/services/user-images-page.service.ts | 9 ++- .../audit/audit-grid/audit-grid.component.ts | 42 ++++++------ .../src/app/resources/images/images.component.html | 35 ++++++---- .../src/app/resources/images/images.component.scss | 31 +++++++++ .../src/app/resources/images/images.component.ts | 46 +++++++++++-- .../src/app/resources/images/images.config.ts | 4 ++ .../src/app/resources/images/images.model.ts | 8 ++- .../src/app/resources/images/images.service.ts | 11 ++++ .../resources-grid/resources-grid.component.html | 4 +- .../webapp/src/app/resources/resources.module.ts | 6 +- .../confirmation-dialog.component.scss | 36 +++++------ .../share-image/share-image.component.html | 39 +++++++++++ .../share-image/share-image.component.scss | 40 ++++++++++++ .../share-image/share-image.component.ts | 60 +++++++++++++++++ .../share-image/share-image.module.ts} | 37 ++++------- .../src/app/shared/navbar/navbar.component.html | 6 +- .../webapp/src/app/shared/time-picker/index.ts | 2 +- .../src/app/shared/time-picker/ticker.component.ts | 8 +-- 46 files changed, 449 insertions(+), 161 deletions(-) diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 5f558e9c6..595e358ee 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -68,21 +68,24 @@ As soon as DataLab is deployed by an infrastructure provisioning team and you re DataLab Web Application authenticates users against: -- OpenLdap; -- Cloud Identity and Access Management service user validation; -- KeyCloak integration for seamless SSO experience *; +As soon as DataLab is deployed by an infrastructure provisioning team and you received DataLab URL, your username and password – open DataLab login page, fill in your credentials and hit Login. + +DataLab Web Application authenticates users against: + +- KeyCloak integration for seamless SSO experience; * NOTE: in case has been installed and configured to use SSO, please click on "Login with SSO" and use your corporate credentials | Login error messages | Reason | |------------------------------------|----------------------------------------------------------------------------------| -| Username or password is invalid |The username provided:<br>doesn’t match any LDAP user OR<br>there is a type in the password field | -| Please contact AWS administrator to create corresponding IAM User | The user name provided:<br>exists in LDAP BUT:<br>doesn’t match any of IAM users in AWS | -| Please contact AWS administrator to activate your Access Key | The username provided:<br>exists in LDAP BUT:<br>IAM user doesn’t have a single Access Key\* created OR<br>IAM user’s Access Key is Inactive | +| Invalid username or password. |The username provided: doesn’t match any LDAP user OR there is a type in the password field | + -\* Please refer to official documentation from Amazon to figure out how to manage Access Keys for your AWS Account: http://docs.aws.amazon.com/general/latest/gr/managing-aws-access-keys.html +To stop working with DataLab - click on user icon <img src="doc/user.png" alt="user" width="20"> at the top right corner of DataLab and hit "Log out from account" button: -To stop working with DataLab - click on Log Out link at the top right corner of DataLab. +<p align="center" class="facebox-popup"> + <img src="doc/user_information.png" alt="User information" width="300"> +</p> After login user sees warning in case of exceeding quota or close to this limit. @@ -107,9 +110,9 @@ To do this click on “Upload” button on “Projects” page, select your pers Please note, that you need to have a key pair combination (public and private key) to work with DataLab. To figure out how to create public and private key, please click on “Where can I get public key?” on “Projects” page. DataLab build-in wiki page guides Windows, MasOS and Linux on how to generate SSH key pairs quickly. -Creation of Project starts after hitting "Create" button. This process is a one-time operation for each Data Scientist and it might take up-to 10 minutes for DataLab to setup initial infrastructure for you. During this process project is in status "Creating". +Creation of Project starts after hitting "Create" button. This process is a one-time operation for each Data Scientist and it might take up-to 25 minutes for DataLab to setup initial infrastructure for you. During this process project is in status "Creating". -As soon as Project is created, Data Scientist can create notebook server on “Resources” page. The message “To start working, please create new environment” is appeared on “Resources” page: +As soon as Project is created, Data Scientist can create notebook server in “Resources” section on "Instances" page . The message “To start working, please create new environment” is appeared on “Instances” page:  @@ -119,7 +122,7 @@ As soon as Project is created, Data Scientist can create notebook server on “ ## Create notebook server <a name="notebook_create"></a> -To create new analytical environment from “Resources” page click on "Create new" button. +To create new analytical environment from “Instances” page click on "Create new" button. The "Create analytical tool" popup shows up. Data Scientist can choose the preferred project, endpoint and analytical tool. Adding new analytical toolset is supported by architecture, so you can expect new templates to show up in upcoming releases. Currently by means of DataLab, Data Scientists can select between any of the following templates: @@ -139,7 +142,7 @@ Currently by means of DataLab, Data Scientists can select between any of the fol After specifying desired template, you should fill in the “Name” and “Instance shape”. -Keep in mind that "Name" field – is just for visual differentiation between analytical tools on “resources” dashboard. +Keep in mind that "Name" field – is just for visual differentiation between analytical tools on “Instances” dashboard. Instance shape dropdown, contains configurable list of shapes, which should be chosen depending on the type of analytical work to be performed. Following groups of instance shapes are showing up with default setup configuration: @@ -199,7 +202,7 @@ On every analytical tool instance you can install additional libraries by clicki After clicking you see the window with 4 fields: - Field for selecting an active resource to install libraries -- Field for selecting group of packages (apt/yum, Python 2, Python 3, R, Java, Others) +- Field for selecting group of packages (apt/yum, Python 3, R, Java, Others) - Field for search available packages with autocomplete feature (if it's gained) except Java dependencies. For Java library you should enter using the next format: "groupID:artifactID:versionID" - Field for library version. It's an optional field. @@ -211,7 +214,7 @@ You need to wait for a while after resource and group choosing till list of all **Note:** Apt or Yum packages depend on your DataLab OS family. -**Note:** In group Others you can find other Python (2/3) packages, which haven't classifiers of version. +**Note:** In group Others you can find other Python (3) packages, which haven't classifiers of version. After selecting library, you can see it in the midle of the window and can delete it from this list before installation. @@ -251,6 +254,18 @@ To create new analytical environment from custom image click on "Create new" but Before clicking "Create" button you should choose the image from "Select AMI" and fill in the "Name" and "Instance shape". For Deeplearning notebook on GCP there is also a list of predefined images. +In addition, you can view the list of all images which are created by you and shared with you on the "Images" page: + + + +You can share the image in Created status with all users in the project or terminate it if you are image creator/owner. Creator is the user who has created the image. +Owner is the user who is owning the right to manage image and perform Share and Terminate actions. The owners are Creator and Admin. +To share or terminate image click on a gear icon <img src="doc/gear_icon.png" alt="gear" width="20"> in the "Actions" menu for a needed image and hit "Terminate" or "Share" button appropriately. + +<p align="center"> + <img src="doc/image_action_menu.png" alt="Image action menu" width="150"> +</p> + -------------------------- ## Stop Notebook server <a name="notebook_stop"></a> @@ -362,9 +377,15 @@ Since Computational resource is up and running - you are now able to leverage cl To do that open any of the analytical tools and select proper kernel/interpreter: -**Jupyter** – go to Kernel and choose preferable interpreter between local and Computational resource ones. Currently we have added support of Python 2 (only for local kernel)/3, Spark, Scala, R in Jupyter. +**Jupyter** – go to Kernel and choose preferable interpreter between local and Computational resource ones. Currently we have added support of Python 3, Spark, Scala, R in Jupyter. + + + +As you know, you can install library thanks to [Manage libraries functionality](#manage_libraries), but in addition you are supposed to install library via Jupyter cell using the next command (i.e., for Python group): - +<p align="center" class="facebox-popup"> + <img src="doc/library_magic_usage.png" alt="Library magic usage" width="200"> +</p> **Zeppelin** – go to Interpreter Biding menu and switch between local and Computational resource there. Once needed interpreter is selected click on "Save". @@ -373,7 +394,6 @@ To do that open any of the analytical tools and select proper kernel/interpreter Insert following “magics” before blocks of your code to start executing your analytical jobs: - interpreter\_name.%spark – for Scala and Spark; -- interpreter\_name.%pyspark – for Python2; - interpreter\_name.%pyspark3 – for Python3; - interpreter\_name.%sparkr – for R; @@ -534,8 +554,8 @@ Also clicking on "Circle" button you can uncommit or revert changes. You are able to access to cloud buckets via DataLab Web UI. There are two ways to open bucket browser: -- clicking on Notebook name on the "List of resources" page, where there is an "Open bucket browser" link; -- clicking on "Bucket browser" bucket on the "List of resources" page. +- clicking on Notebook name on the "Instances" page, where there is an "Open bucket browser" link; +- clicking on "Bucket browser" bucket on the "Instances" page.  @@ -557,6 +577,18 @@ In the bucket browser you are supposed to: -------------------------------- # Administration <a name="administration"></a> +There are four pages in the "Administration" panel: + +<p align="center"> + <img src="doc/administration_section.png" alt="Administration section" width="150"> +</p> + +- "Users" page, where administrator can assign appropriate permisions for users; +- "Projects" page, where administrator can manage a project; +- "Resources" page, where administrator monitor and manage project resources; +- "Configuration" page, where administrator can view and change configuration files and restart DataLab services. + + ## Manage roles <a name="manage_roles"></a> Administrator can choose what instance shape(s), notebook(s) and computational resource are supposed to create for certain group(s) or user(s). Administrator can also assign administrator per project, who is able to manage roles within particular project. @@ -744,7 +776,7 @@ You are able to view: - who did the action - what the action was done -Furthermore on the center of header you can choose period of report in datepicker. +Furthermore, on the center of header you can choose period of report in datepicker.  diff --git a/billing_page.png b/billing_page.png new file mode 100644 index 000000000..a44567440 Binary files /dev/null and b/billing_page.png differ diff --git a/doc/administration_section.png b/doc/administration_section.png new file mode 100644 index 000000000..19e2e88d8 Binary files /dev/null and b/doc/administration_section.png differ diff --git a/doc/audit_page.png b/doc/audit_page.png index 62bf041a1..c1bac87ab 100644 Binary files a/doc/audit_page.png and b/doc/audit_page.png differ diff --git a/doc/billing_page.png b/doc/billing_page.png index df01a7eb8..a44567440 100644 Binary files a/doc/billing_page.png and b/doc/billing_page.png differ diff --git a/doc/bucket_button.png b/doc/bucket_button.png index 21ec452c8..68be640fe 100644 Binary files a/doc/bucket_button.png and b/doc/bucket_button.png differ diff --git a/doc/configuration_page.png b/doc/configuration_page.png index 7bb50f749..17c42870b 100644 Binary files a/doc/configuration_page.png and b/doc/configuration_page.png differ diff --git a/doc/configuration_page1.png b/doc/configuration_page1.png index 5f30d038a..08c97283d 100644 Binary files a/doc/configuration_page1.png and b/doc/configuration_page1.png differ diff --git a/doc/configuration_page_prov.png b/doc/configuration_page_prov.png index 0faee799b..d9cc361bc 100644 Binary files a/doc/configuration_page_prov.png and b/doc/configuration_page_prov.png differ diff --git a/doc/configuration_page_restart.png b/doc/configuration_page_restart.png index 479e0633a..cd6e015b6 100644 Binary files a/doc/configuration_page_restart.png and b/doc/configuration_page_restart.png differ diff --git a/doc/environment_management.png b/doc/environment_management.png index 45a8b1336..2d75cd091 100644 Binary files a/doc/environment_management.png and b/doc/environment_management.png differ diff --git a/doc/image_action_menu.png b/doc/image_action_menu.png new file mode 100644 index 000000000..314de80d6 Binary files /dev/null and b/doc/image_action_menu.png differ diff --git a/doc/images_main.png b/doc/images_main.png new file mode 100644 index 000000000..110042921 Binary files /dev/null and b/doc/images_main.png differ diff --git a/doc/jupyter_kernel.png b/doc/jupyter_kernel.png index d718824a6..d8c0f873d 100644 Binary files a/doc/jupyter_kernel.png and b/doc/jupyter_kernel.png differ diff --git a/doc/library_magic_usage.png b/doc/library_magic_usage.png new file mode 100644 index 000000000..29139265a Binary files /dev/null and b/doc/library_magic_usage.png differ diff --git a/doc/main_page.png b/doc/main_page.png index 3c3d1e8c5..de5f64c8e 100644 Binary files a/doc/main_page.png and b/doc/main_page.png differ diff --git a/doc/main_page_filter.png b/doc/main_page_filter.png index bf0dc2335..4ede541ff 100644 Binary files a/doc/main_page_filter.png and b/doc/main_page_filter.png differ diff --git a/doc/upload_or_generate_user_key.png b/doc/upload_or_generate_user_key.png index b029c2807..b5d50e405 100644 Binary files a/doc/upload_or_generate_user_key.png and b/doc/upload_or_generate_user_key.png differ diff --git a/doc/user.png b/doc/user.png new file mode 100644 index 000000000..151271730 Binary files /dev/null and b/doc/user.png differ diff --git a/doc/user_information.png b/doc/user_information.png new file mode 100644 index 000000000..1e9ce4c75 Binary files /dev/null and b/doc/user_information.png differ diff --git a/infrastructure-provisioning/src/base/scripts/create_ssh_user.py b/infrastructure-provisioning/src/base/scripts/create_ssh_user.py index 183295cf5..3af70e5a9 100644 --- a/infrastructure-provisioning/src/base/scripts/create_ssh_user.py +++ b/infrastructure-provisioning/src/base/scripts/create_ssh_user.py @@ -57,7 +57,7 @@ def ensure_ssh_user(initial_user, os_user, sudo_group): if __name__ == "__main__": print("Configure connections") global conn - conn = datalab.fab.init_datalab_connection(args.hostname, args.initial_user, args.keyfile) + conn = datalab.fab.init_datalab_connection(args.hostname, args.initial_user, args.keyfile, args.os_user) print("Creating ssh user: {}".format(args.os_user)) try: ensure_ssh_user(args.initial_user, args.os_user, args.sudo_group) diff --git a/infrastructure-provisioning/src/general/lib/os/fab.py b/infrastructure-provisioning/src/general/lib/os/fab.py index 33f1ef2ae..add3157c3 100644 --- a/infrastructure-provisioning/src/general/lib/os/fab.py +++ b/infrastructure-provisioning/src/general/lib/os/fab.py @@ -40,22 +40,27 @@ from patchwork import files # general functions for all resources -def init_datalab_connection(hostname, username, keyfile): +def init_datalab_connection(hostname, username, keyfile, reserve_user=''): try: global conn - attempt = 0 - while attempt < 15: - logging.info('connection attempt {}'.format(attempt)) - conn = Connection(host=hostname, user=username, connect_kwargs={'banner_timeout': 200, - 'key_filename': keyfile}) - conn.config.run.echo = True - try: - conn.run('hostname') + if reserve_user: + users = [username, reserve_user] + else: + users = [username] + for user in users: + attempt = 0 + while attempt < 15: + logging.info('connection attempt {} with user {}'.format(attempt, user)) + conn = Connection(host=hostname, user=user, connect_kwargs={'banner_timeout': 200, + 'key_filename': keyfile}) conn.config.run.echo = True - return conn - except: - attempt += 1 - time.sleep(10) + try: + conn.run('hostname') + conn.config.run.echo = True + return conn + except: + attempt += 1 + time.sleep(10) if attempt == 15: logging.info('Unable to establish connection') raise Exception diff --git a/infrastructure-provisioning/src/general/scripts/aws/common_create_notebook_image.py b/infrastructure-provisioning/src/general/scripts/aws/common_create_notebook_image.py index 3c08b5e91..824e6070b 100644 --- a/infrastructure-provisioning/src/general/scripts/aws/common_create_notebook_image.py +++ b/infrastructure-provisioning/src/general/scripts/aws/common_create_notebook_image.py @@ -67,6 +67,11 @@ if __name__ == "__main__": instance_name=image_conf['instance_name'], image_name=image_conf['full_image_name'], tags=json.dumps(image_conf['tags'])) + + logging.info("Image id from actions.lib.py {}".format(image_id)) + if not image_id : + raise Exception("Image can not be created from not running instances") + logging.info("Image was successfully created. It's name is {}".format(image_conf['full_image_name'])) with open("/root/result.json", 'w') as result: @@ -79,4 +84,4 @@ if __name__ == "__main__": result.write(json.dumps(res)) except Exception as err: datalab.fab.append_result("Failed to create image from notebook", str(err)) - sys.exit(1) + sys.exit(1) \ No newline at end of file diff --git a/infrastructure-provisioning/terraform/azure/endpoint/main/network.tf b/infrastructure-provisioning/terraform/azure/endpoint/main/network.tf index 85c9d73de..5ef29452a 100644 --- a/infrastructure-provisioning/terraform/azure/endpoint/main/network.tf +++ b/infrastructure-provisioning/terraform/azure/endpoint/main/network.tf @@ -53,6 +53,7 @@ resource "azurerm_subnet" "endpoint-subnet" { resource_group_name = data.azurerm_resource_group.data-endpoint-resource-group.name virtual_network_name = data.azurerm_virtual_network.data-endpoint-network.name address_prefixes = [var.subnet_cidr] + service_endpoints = ["Microsoft.Storage"] } data "azurerm_subnet" "data-endpoint-subnet" { diff --git a/services/self-service/src/main/java/com/epam/datalab/backendapi/service/impl/ImageExploratoryServiceImpl.java b/services/self-service/src/main/java/com/epam/datalab/backendapi/service/impl/ImageExploratoryServiceImpl.java index 842e3b7ec..1f1623e3d 100644 --- a/services/self-service/src/main/java/com/epam/datalab/backendapi/service/impl/ImageExploratoryServiceImpl.java +++ b/services/self-service/src/main/java/com/epam/datalab/backendapi/service/impl/ImageExploratoryServiceImpl.java @@ -268,7 +268,6 @@ public class ImageExploratoryServiceImpl implements ImageExploratoryService { .filter(img -> !img.getUser().equals(userInfo.getName())) .filter(img -> UserRoles.checkAccess(userInfo, RoleType.IMAGE, img.getFullName(), userInfo.getRoles())) - .peek(img -> img.setShared(true)) .collect(Collectors.toList()); log.info("Shared with user {} images : {}", userInfo.getName(), sharedImages); return sharedImages; @@ -280,7 +279,6 @@ public class ImageExploratoryServiceImpl implements ImageExploratoryService { .filter(img -> !img.getUser().equals(userInfo.getName())) .filter(img -> img.getDockerImage().equals(dockerImage) && img.getProject().equals(project) && img.getEndpoint().equals(endpoint)) .filter(img -> UserRoles.checkAccess(userInfo, RoleType.IMAGE, img.getFullName(), userInfo.getRoles())) - .peek(img -> img.setShared(true)) .collect(Collectors.toList()); log.info("Found shared with user {} images {}", userInfo.getName(), sharedImages); return sharedImages; @@ -292,7 +290,6 @@ public class ImageExploratoryServiceImpl implements ImageExploratoryService { .filter(img -> !img.getUser().equals(userInfo.getName())) .filter(img -> img.getProject().equals(project) ) .filter(img -> UserRoles.checkAccess(userInfo, RoleType.IMAGE, img.getFullName(), userInfo.getRoles())) - .peek(img -> img.setShared(true)) .collect(Collectors.toList()); log.info("Found shared with user {} images {}", userInfo.getName(), sharedImages); return sharedImages; @@ -301,6 +298,7 @@ public class ImageExploratoryServiceImpl implements ImageExploratoryService { private boolean isSharedImage(String imageFullName){ String anyUser = "$anyuser"; List<UserRoleDTO> imageRoles = userRoleDAO.findAll().stream() + .filter( r -> r.getType().equals(UserRoleDTO.Type.IMAGE)) .filter(r -> r.getImages().contains(imageFullName)) .filter( r -> (r.getGroups().contains(anyUser) && r.getGroups().size() >= 2) || (!r.getGroups().contains(anyUser) && !r.getGroups().isEmpty())) diff --git a/services/self-service/src/main/resources/webapp/package-lock.json b/services/self-service/src/main/resources/webapp/package-lock.json index 2f5426228..305e75aaf 100644 --- a/services/self-service/src/main/resources/webapp/package-lock.json +++ b/services/self-service/src/main/resources/webapp/package-lock.json @@ -4450,9 +4450,9 @@ } }, "node_modules/codelyzer": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-6.0.1.tgz", - "integrity": "sha512-cOyGQgMdhnRYtW2xrJUNrNYDjEgwQ+BrE2y93Bwz3h4DJ6vJRLfupemU5N3pbYsUlBHJf0u1j1UGk+NLW4d97g==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-6.0.2.tgz", + "integrity": "sha512-v3+E0Ucu2xWJMOJ2fA/q9pDT/hlxHftHGPUay1/1cTgyPV5JTHFdO9hqo837Sx2s9vKBMTt5gO+lhF95PO6J+g==", "dev": true, "dependencies": { "@angular/compiler": "9.0.0", @@ -4471,8 +4471,8 @@ "zone.js": "~0.10.3" }, "peerDependencies": { - "@angular/compiler": ">=2.3.1 <12.0.0 || ^11.0.0-next || ^11.1.0-next || ^11.2.0-next", - "@angular/core": ">=2.3.1 <12.0.0 || ^11.0.0-next || ^11.1.0-next || ^11.2.0-next", + "@angular/compiler": ">=2.3.1 <13.0.0 || ^12.0.0-next || ^12.1.0-next || ^12.2.0-next", + "@angular/core": ">=2.3.1 <13.0.0 || ^12.0.0-next || ^12.1.0-next || ^12.2.0-next", "tslint": "^5.0.0 || ^6.0.0" } }, @@ -9197,9 +9197,9 @@ } }, "node_modules/moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", "engines": { "node": "*" } @@ -14251,7 +14251,7 @@ "node_modules/watchpack-chokidar2/node_modules/glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", "dev": true, "optional": true, "dependencies": { @@ -19069,9 +19069,9 @@ "dev": true }, "codelyzer": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-6.0.1.tgz", - "integrity": "sha512-cOyGQgMdhnRYtW2xrJUNrNYDjEgwQ+BrE2y93Bwz3h4DJ6vJRLfupemU5N3pbYsUlBHJf0u1j1UGk+NLW4d97g==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-6.0.2.tgz", + "integrity": "sha512-v3+E0Ucu2xWJMOJ2fA/q9pDT/hlxHftHGPUay1/1cTgyPV5JTHFdO9hqo837Sx2s9vKBMTt5gO+lhF95PO6J+g==", "dev": true, "requires": { "@angular/compiler": "9.0.0", @@ -22804,9 +22804,9 @@ } }, "moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==" }, "moment-timezone": { "version": "0.5.31", @@ -26691,7 +26691,7 @@ "glob-parent": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", "dev": true, "optional": true, "requires": { 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 0239fc9e8..8cc601782 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 @@ -65,7 +65,7 @@ const routes: Routes = [ canActivate: [AuthorizationGuard] }, { - path: 'billing_report', + path: 'billing', component: ReportingComponent, canActivate: [AuthorizationGuard, CloudProviderGuard] }, @@ -79,12 +79,12 @@ const routes: Routes = [ // component: OdahuComponent, // canActivate: [AuthorizationGuard, AdminGuard], // }, { - path: 'roles', + path: 'users', component: RolesComponent, canActivate: [AuthorizationGuard, AdminGuard], }, { - path: 'environment_management', + path: 'resources', component: ManagementComponent, canActivate: [AuthorizationGuard, AdminGuard] }, 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 cd39b0e67..f97ca1da1 100644 --- a/services/self-service/src/main/resources/webapp/src/app/core/services/applicationServiceFacade.service.ts +++ b/services/self-service/src/main/resources/webapp/src/app/core/services/applicationServiceFacade.service.ts @@ -17,13 +17,14 @@ * under the License. */ -import {Injectable} from '@angular/core'; -import {Observable} from 'rxjs'; -import {HttpClient} from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; -import {Dictionary} from '../collections'; -import {environment} from '../../../environments/environment'; -import {HTTPMethod} from '../util'; +import { Dictionary } from '../collections'; +import { environment } from '../../../environments/environment'; +import { HTTPMethod } from '../util'; +import { ShareImageAllUsersParams } from '../../resources/images'; // we can now access environment.apiUrl const API_URL = environment.apiUrl; @@ -44,6 +45,7 @@ export class ApplicationServiceFacade { private static readonly PROVISIONED_RESOURCES = 'provisioned_resources'; private static readonly EXPLORATORY_ENVIRONMENT = 'exploratory_environment'; private static readonly IMAGE = 'image'; + private static readonly SHARE_ALL = 'share_all'; private static readonly SCHEDULER = 'scheduler'; private static readonly TEMPLATES = 'templates'; private static readonly COMPUTATION_TEMPLATES = 'computation_templates'; @@ -187,6 +189,13 @@ export class ApplicationServiceFacade { null); } + buildShareImageAllUsers(params: ShareImageAllUsersParams): Observable<any> { + return this.buildRequest(HTTPMethod.POST, + this.requestRegistry.Item(ApplicationServiceFacade.SHARE_ALL), + params + ); + } + public buildGetTemplatesRequest(params): Observable<any> { return this.buildRequest(HTTPMethod.GET, this.requestRegistry.Item(ApplicationServiceFacade.TEMPLATES) + params, @@ -729,8 +738,6 @@ export class ApplicationServiceFacade { '/api/infrastructure_templates'); this.requestRegistry.Add(ApplicationServiceFacade.COMPUTATION_TEMPLATES, '/infrastructure_provision/computational_resources'); - this.requestRegistry.Add(ApplicationServiceFacade.IMAGE, - '/api/infrastructure_provision/exploratory_environment/image'); this.requestRegistry.Add(ApplicationServiceFacade.SCHEDULER, '/api/infrastructure_provision/exploratory_environment/scheduler'); @@ -742,9 +749,17 @@ export class ApplicationServiceFacade { this.requestRegistry.Add(ApplicationServiceFacade.COMPUTATIONAL_RESOURCES_DATAENGINE, '/infrastructure_provision/computational_resources/dataengine'); // spark (azure|aws) + this.requestRegistry.Add(ApplicationServiceFacade.COMPUTATIONAL_RESOURCES_TEMLATES, '/api/infrastructure_templates/computational_templates'); + + // Images + this.requestRegistry.Add(ApplicationServiceFacade.IMAGE, + '/api/infrastructure_provision/exploratory_environment/image'); + this.requestRegistry.Add(ApplicationServiceFacade.SHARE_ALL, + '/api/infrastructure_provision/exploratory_environment/image/share'); + // Bucket browser this.requestRegistry.Add(ApplicationServiceFacade.BUCKET, '/api/bucket'); diff --git a/services/self-service/src/main/resources/webapp/src/app/core/services/user-images-page.service.ts b/services/self-service/src/main/resources/webapp/src/app/core/services/user-images-page.service.ts index 05e9e9077..aca404052 100644 --- a/services/self-service/src/main/resources/webapp/src/app/core/services/user-images-page.service.ts +++ b/services/self-service/src/main/resources/webapp/src/app/core/services/user-images-page.service.ts @@ -23,7 +23,7 @@ import { catchError } from 'rxjs/operators'; import { ErrorUtils } from '../util'; import { ApplicationServiceFacade } from './applicationServiceFacade.service'; -import { ProjectModel } from '../../resources/images'; +import { ProjectModel, ShareImageAllUsersParams } from '../../resources/images'; @Injectable() export class UserImagesPageService { @@ -36,4 +36,11 @@ export class UserImagesPageService { catchError(ErrorUtils.handleServiceError) ); } + + shareImageAllUsers(params: ShareImageAllUsersParams) { + return this.applicationServiceFacade.buildShareImageAllUsers(params) + .pipe( + catchError(ErrorUtils.handleServiceError) + ); + } } 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 index 090dcb33e..866077106 100644 --- 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 @@ -126,10 +126,10 @@ export class AuditGridComponent implements OnInit { public openActionInfo(element: AuditItem): void { if (element.type === 'GROUP' && element.info.indexOf('role') !== -1) { - this.dialog.open(AuditInfoDialogComponent, + this.dialog.open(AuditInfoDialogComponent, { data: { element, dialogSize: 'big' }, panelClass: 'modal-xl-m' }); } else { - this.dialog.open(AuditInfoDialogComponent, + this.dialog.open(AuditInfoDialogComponent, { data: {element, dialogSize: 'small' }, panelClass: 'modal-md' }); } } @@ -149,17 +149,17 @@ export class AuditGridComponent implements OnInit { this.lastItem = this.showItemsPrPage; } else if (action === 'previous') { this.firstItem = this.firstItem - this.showItemsPrPage; - this.lastItem = this.lastItem % this.showItemsPrPage === 0 - ? this.lastItem - this.showItemsPrPage + this.lastItem = this.lastItem % this.showItemsPrPage === 0 + ? this.lastItem - this.showItemsPrPage : this.lastItem - (this.lastItem % this.showItemsPrPage); } else if (action === 'next') { this.firstItem = this.firstItem + this.showItemsPrPage; - this.lastItem = (this.lastItem + this.showItemsPrPage) > this.allItems - ? this.allItems + this.lastItem = (this.lastItem + this.showItemsPrPage) > this.allItems + ? this.allItems : this.lastItem + this.showItemsPrPage; } else if (action === 'last') { - this.firstItem = this.allItems % this.showItemsPrPage === 0 - ? this.allItems - this.showItemsPrPage + this.firstItem = this.allItems % this.showItemsPrPage === 0 + ? this.allItems - this.showItemsPrPage : this.allItems - (this.allItems % this.showItemsPrPage) + 1; this.lastItem = this.allItems; } @@ -185,12 +185,12 @@ export class AuditGridComponent implements OnInit { <h4 class="modal-title">{{data.element.action | convertaction}}</h4> <button type="button" class="close" (click)="dialogRef.close()">×</button> </header> - <div - mat-dialog-content - class="content audit-info-content" + <div + mat-dialog-content + class="content audit-info-content" [ngClass]="{'pb-40': actionList[0].length > 1}" > - <mat-list + <mat-list *ngIf="actionList[0].length > 1 && data.element.action !== 'FOLLOW_LINK' || data.element.info.indexOf('Update quota') !== -1;else message" > @@ -203,9 +203,9 @@ export class AuditGridComponent implements OnInit { <div class="scrolling-content mat-list-wrapper" id="scrolling"> <mat-list-item class="list-item" *ngFor="let action of actionList"> - <div - *ngIf="(data.element.action === 'upload' && action[0] === 'File(s)') - || (data.element.action === 'download' && action[0] === 'File(s)');else multiAction" + <div + *ngIf="(data.element.action === 'upload' && action[0] === 'File(s)') + || (data.element.action === 'download' && action[0] === 'File(s)');else multiAction" class="info-item-title" > File @@ -213,12 +213,12 @@ export class AuditGridComponent implements OnInit { <ng-template #multiAction> <div class="info-item-title" [ngClass]="{'same-column-width': data.dialogSize === 'small'}">{{action[0]}}</div> </ng-template> - <div - class="info-item-data" - [ngClass]="{'same-column-width': data.dialogSize === 'small'}" + <div + class="info-item-data" + [ngClass]="{'same-column-width': data.dialogSize === 'small'}" *ngIf="action[0] === 'File(s)'" > - <div + <div class="file-description ellipsis" *ngFor="let description of action[1]?.split(',')" [matTooltip]="description" @@ -228,7 +228,7 @@ export class AuditGridComponent implements OnInit { </div> </div> <div class="info-item-data" [ngClass]="{'same-column-width': data.dialogSize === 'small'}" *ngIf="action[0] !== 'File(s)'"> - <div + <div *ngFor="let description of action[1]?.split(',')" [matTooltip]="description" class="ellipsis" @@ -353,4 +353,4 @@ export class AuditInfoDialogComponent { } this.actionList = data.element.info.split('\n').map(v => v.split(':')).filter(v => v[0] !== ''); } -} \ No newline at end of file +} diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.html index 16f7bf728..e239fe1e7 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.html +++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.html @@ -25,16 +25,18 @@ [ngClass]="{'disabled-select': !isProjectsMoreThanOne}" > <mat-form-field> - <mat-label>Select project</mat-label> + <mat-label>{{placeholder}}</mat-label> <mat-select disableOptionCentering + name="project" + [(value)]="activeProjectName" panelClass="top-select scrolling" - [disabled]="!projectList.length" + [disabled]="!isProjectsMoreThanOne" > <mat-option *ngIf="isProjectsMoreThanOne" - (click)="onSelectClick('')" + (click)="onSelectClick()" > Show all </mat-option> @@ -49,7 +51,7 @@ Projects list is empty </mat-option> </mat-select> - <button class="caret" [disabled]="false"> + <button class="caret" [disabled]="!isProjectsMoreThanOne"> <i class="material-icons">keyboard_arrow_down</i> </button> </mat-form-field> @@ -90,7 +92,7 @@ </div> </span> <span> - <button mat-raised-button class="butt"> + <button mat-raised-button class="butt" (click)="onRefresh()"> <i class="material-icons highlight">autorenew</i> Refresh </button> @@ -104,7 +106,7 @@ <ng-container matColumnDef="checkbox"> <th mat-header-cell *matHeaderCellDef class="image-checkbox--wrapper"> <div class="header-cell--wrapper"> - <span> + <span *ngIf="dataSource.length" > <datalab-checkbox (click)="allCheckboxToggle()" [checked]="checkboxSelected" @@ -147,8 +149,7 @@ </div> </th> <td mat-cell *matCellDef="let element"> - <span class="date-item"> {{element.timestamp | date: 'yyyy-MM-dd'}} </span> - <span> {{element.timestamp | date: 'HH:mm:ss'}} </span> + <span> {{element.timestamp | localDate : 'short'}} </span> </td> </ng-container> @@ -189,7 +190,7 @@ </th> <td mat-cell *matCellDef="let element"> <div class="shared-status--wrapper"> - <span class="shared-status"> {{element.isShader ? sharedStatus.shared : sharedStatus.private}} </span> + <span class="shared-status"> {{element.shared ? sharedStatus.shared : sharedStatus.private}} </span> <span class="currency_details" > <i class="material-icons">help_outline</i> </span> @@ -237,16 +238,22 @@ matTooltip="Unable to terminate notebook because at least one compute is in progress" matTooltipPosition="above" > - <div> + <button class="action-button__share"> <i class="material-icons">phonelink_off</i> <span>Terminate</span> - </div> + </button> </li> - <li> - <div> + <li [matTooltip]="'This image cannot be shared by you. Contact Admin user'" + matTooltipPosition="above" + [matTooltipDisabled]="userName === element.user && element.status === 'CREATED'"> + <button + class="action-button__share" + (click)="onShare(element)" + [disabled]="userName !== element.user || element.status !== 'CREATED'" + > <i class="material-icons">create</i> <span>Share</span> - </div> + </button> </li> </ul> </bubble-up> diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.scss b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.scss index 85487f421..c032060ce 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.scss +++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.scss @@ -85,3 +85,34 @@ background-color: white; opacity: 1; } + +.action-button__share { + display: flex; + align-items: start; + width: 100%; + padding: 10px 15px; + background-color: transparent; + color: rgb(87, 114, 137); + border: none; + outline: none; + + &:hover { + color: #35afd5; + cursor: pointer; + } + + &:disabled { + background-color: rgba(0, 0, 0, 0.12); + color: rgba(0, 0, 0, 0.26); + cursor: not-allowed; + transition: all 0.45s ease-in-out; + } +} + +.list-menu { + padding: 0 !important; +} + +.list-unstyled > li { + margin: 0 !important; +} diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.ts b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.ts index 7e0e449d3..b5dcc7826 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.ts +++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.component.ts @@ -24,7 +24,10 @@ import { ToastrService } from 'ngx-toastr'; import { GeneralEnvironmentStatus } from '../../administration/management/management.model'; import { HealthStatusService, UserImagesPageService } from '../../core/services'; import { ImageModel, ProjectModel } from './images.model'; -import { Image_Table_Column_Headers, Image_Table_Titles, Shared_Status } from './images.config'; +import { Image_Table_Column_Headers, Image_Table_Titles, Localstorage_Key, Shared_Status } from './images.config'; +import { MatDialog } from '@angular/material/dialog'; +import { ShareImageComponent } from '../../shared/modal-dialog/share-image/share-image.component'; +import { ImagesService } from './images.service'; @Component({ selector: 'datalab-images', @@ -44,18 +47,26 @@ export class ImagesComponent implements OnInit { dataSource: ImageModel[] = []; checkboxSelected: boolean = false; projectList: string[] = []; + activeProjectName: string = ''; + userName!: string; + + readonly placeholder: string = 'Select project'; readonly sharedStatus: typeof Shared_Status = Shared_Status; + private cashedImageListData: ProjectModel[] = []; constructor( private healthStatusService: HealthStatusService, public toastr: ToastrService, - private userImagesPageService: UserImagesPageService + private userImagesPageService: UserImagesPageService, + private dialog: MatDialog, + private imagesService: ImagesService ) { } ngOnInit(): void { this.getEnvironmentHealthStatus(); this.getUserImagePageInfo(); + this.getUserName(); } onCheckboxClick(element: ImageModel): void { @@ -76,12 +87,28 @@ export class ImagesComponent implements OnInit { this.isActionsOpen = !this.isActionsOpen; } - onSelectClick(projectName: string): void { + onSelectClick(projectName: string = ''): void { if (!projectName) { this.dataSource = this.getImageList(); + return; } - const { images } = this.cashedImageListData.find(({project}) => project === projectName); - this.dataSource = [...images]; + const currentProject = this.cashedImageListData.find(({project}) => project === projectName); + this.dataSource = [...currentProject.images]; + this.activeProjectName = currentProject.project; + } + + onRefresh(): void { + this.getUserImagePageInfo(); + this.activeProjectName = ''; + } + + onShare(image: ImageModel): void { + this.dialog.open(ShareImageComponent, { + data: { + image + }, + panelClass: 'modal-sm' + }).afterClosed().subscribe(() => this.initImageTable(this.imagesService.projectList)); } private getImageList(): ImageModel[] { @@ -105,15 +132,24 @@ export class ImagesComponent implements OnInit { this.cashedImageListData = imagePageList; this.getProjectList(imagePageList); this.dataSource = this.getImageList(); + + if (imagePageList.length === 1) { + this.activeProjectName = imagePageList[0].project; + } } private getProjectList(imagePageList: ProjectModel[]): void { if (!imagePageList) { return; } + this.projectList = []; imagePageList.forEach(({project}) => this.projectList.push(project)); } + private getUserName(): void { + this.userName = localStorage.getItem(Localstorage_Key.userName); + } + get isProjectsMoreThanOne(): boolean { return this.projectList.length > 1; } diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.config.ts b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.config.ts index b57fbda4d..e6f4d5ec9 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.config.ts +++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.config.ts @@ -44,3 +44,7 @@ export const Image_Table_Titles = <const>[ 'instanceName', 'actions' ]; + +export enum Localstorage_Key { + userName = 'user_name' +} diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.model.ts b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.model.ts index c1c5bf64d..8c0735950 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.model.ts +++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.model.ts @@ -13,8 +13,14 @@ export interface ImageModel { instanceName: string; name: string; project: string; - isShared: boolean; + shared: boolean; status: 'created' | 'creating' | 'terminated' | 'terminating' | 'failed'; user: string; isSelected?: boolean; } + +export interface ShareImageAllUsersParams { + imageName: string; + projectName: string; + endpoint: string; +} diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.service.ts b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.service.ts new file mode 100644 index 000000000..36a9e5963 --- /dev/null +++ b/services/self-service/src/main/resources/webapp/src/app/resources/images/images.service.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@angular/core'; +import { ProjectModel } from './images.model'; + +@Injectable({ + providedIn: 'root' +}) +export class ImagesService { + projectList!: ProjectModel[]; + + constructor() { } +} diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.component.html b/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.component.html index 9dcd2e8e8..c80417e07 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.component.html +++ b/services/self-service/src/main/resources/webapp/src/app/resources/resources-grid/resources-grid.component.html @@ -232,8 +232,8 @@ <li *ngIf="element.status.toLowerCase() === 'stopped' || element.status.toLowerCase() === 'stopping'" matTooltip="{{element.edgeNodeStatus !== 'running' - ? 'Unable to run notebook if edge node is not running.' - : 'Unable to run notebook until it will be stopped.'}}" + ? 'Unable to start notebook if edge node is not running.' + : 'Unable to start notebook until it will be stopped.'}}" matTooltipPosition="above" [matTooltipDisabled]="!isResourcesInProgress(element) && element.status.toLowerCase() !== 'stopping' && element.edgeNodeStatus === 'running'" 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 996c347d4..885328fc2 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 @@ -35,6 +35,8 @@ import { ImagesComponent } from './images/images.component'; import {CheckboxModule} from '../shared/checkbox'; import {BubbleModule} from '../shared'; import { CapitalizeFirstLetterPipeModule } from '../core/pipes'; +import { LocalDatePipeModule } from '../core/pipes/local-date-pipe'; +import { ShareImageModule } from '../shared/modal-dialog/share-image/share-image.module'; @NgModule({ imports: [ @@ -49,7 +51,9 @@ import { CapitalizeFirstLetterPipeModule } from '../core/pipes'; BucketBrowserModule, CheckboxModule, BubbleModule, - CapitalizeFirstLetterPipeModule + CapitalizeFirstLetterPipeModule, + LocalDatePipeModule, + ShareImageModule ], declarations: [ ResourcesComponent, diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/confirmation-dialog/confirmation-dialog.component.scss b/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/confirmation-dialog/confirmation-dialog.component.scss index 10228887b..dd85fe64a 100644 --- a/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/confirmation-dialog/confirmation-dialog.component.scss +++ b/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/confirmation-dialog/confirmation-dialog.component.scss @@ -1,21 +1,21 @@ -/*! - * 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. - */ + /*! + * 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 { .content-box{ diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/share-image/share-image.component.html b/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/share-image/share-image.component.html new file mode 100644 index 000000000..dff6919bf --- /dev/null +++ b/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/share-image/share-image.component.html @@ -0,0 +1,39 @@ +<!-- + ~ 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"> + <header class="dialog-header"> + <h4 class="modal-title">Share Image: <span>{{imageName}}</span></h4> + <button type="button" class="close" (click)="dialogRef.close()">×</button> + </header> + <section class="content"> + <p class="description"> + The image will be shared with the rest of Regular Users on the project with all the data and code. + </p> + <p class="question center"> + Do you want proceed? + </p> + <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)="onYesClick()">Yes + </button> + </div> + </section> +</div> diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/share-image/share-image.component.scss b/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/share-image/share-image.component.scss new file mode 100644 index 000000000..8f765c333 --- /dev/null +++ b/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/share-image/share-image.component.scss @@ -0,0 +1,40 @@ +/*! +* 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. +*/ + +.content { + max-height: 75vh; + margin: 0; + padding: 20px 50px; + font-weight: 600; +} + +.question { + color: #718ba6; +} + +.center { + text-align: center; +} + +.description { + margin-bottom: 20px; + color: #35afd5; + font-size: 14px; + text-align: center; +} diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/share-image/share-image.component.ts b/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/share-image/share-image.component.ts new file mode 100644 index 000000000..e034b10ec --- /dev/null +++ b/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/share-image/share-image.component.ts @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Component, Inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { UserImagesPageService } from '../../../core/services'; +import { ShareImageAllUsersParams } from '../../../resources/images'; +import { ImagesService } from '../../../resources/images/images.service'; + +@Component({ + selector: 'datalab-share-image', + templateUrl: './share-image.component.html', + styleUrls: ['./share-image.component.scss'] +}) +export class ShareImageComponent implements OnInit { + imageName!: string; + + constructor( + public dialogRef: MatDialogRef<ShareImageComponent>, + @Inject(MAT_DIALOG_DATA) public data: any, + private userImagesPageService: UserImagesPageService, + private imagesService: ImagesService + ) { } + + ngOnInit(): void { + this.getImageName(); + } + + getImageName(): void { + this.imageName = this.data.image.name; + } + + onYesClick(): void { + const shareParams: ShareImageAllUsersParams = { + imageName: this.data.image.name, + projectName: this.data.image.project, + endpoint: this.data.image.endpoint + }; + + this.userImagesPageService.shareImageAllUsers(shareParams) + .subscribe(projectList => this.imagesService.projectList = projectList); + this.dialogRef.close(); + } +} diff --git a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.config.ts b/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/share-image/share-image.module.ts similarity index 59% copy from services/self-service/src/main/resources/webapp/src/app/resources/images/images.config.ts copy to services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/share-image/share-image.module.ts index b57fbda4d..bd7a99d1d 100644 --- a/services/self-service/src/main/resources/webapp/src/app/resources/images/images.config.ts +++ b/services/self-service/src/main/resources/webapp/src/app/shared/modal-dialog/share-image/share-image.module.ts @@ -17,30 +17,17 @@ * under the License. */ -export enum Image_Table_Column_Headers { - imageName = 'Image name', - creationDate = 'Creation date', - provider = 'Provider', - imageStatus = 'Image status', - sharedStatus = 'Shared status', - templateName = 'Template name', - instanceName = 'Instance name', - actions = 'Actions', -} +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ShareImageComponent } from './share-image.component'; +import { NotificationDialogComponent } from '../notification-dialog'; -export enum Shared_Status { - shared = 'Shared', - private = 'Private' -} -export const Image_Table_Titles = <const>[ - 'checkbox', - 'imageName', - 'creationDate', - 'provider', - 'imageStatus', - 'sharedStatus', - 'templateName', - 'instanceName', - 'actions' -]; + +@NgModule({ + declarations: [ ShareImageComponent ], + imports: [ CommonModule ], + entryComponents: [ShareImageComponent], + exports: [ ShareImageComponent ] +}) +export class ShareImageModule { } 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 60e635d99..4ec0819a6 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 @@ -146,7 +146,7 @@ <a *ngIf="healthStatus?.billingEnabled" class="sub-nav-item" - [routerLink]="['/billing_report']" + [routerLink]="['/billing']" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{exact:true}" [style.margin-left.px]="isExpanded ? '30' : '0'" @@ -161,7 +161,7 @@ <a class="sub-nav-item" [style.margin-left.px]="isExpanded ? '30' : '0'" - [routerLink]="['/roles']" + [routerLink]="['/users']" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{exact:true}" > @@ -186,7 +186,7 @@ <a class="sub-nav-item" [style.margin-left.px]="isExpanded ? '30' : '0'" - [routerLink]="['/environment_management']" + [routerLink]="['/resources']" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{exact:true}" > diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/time-picker/index.ts b/services/self-service/src/main/resources/webapp/src/app/shared/time-picker/index.ts index 4133045d1..0fe6caab5 100644 --- a/services/self-service/src/main/resources/webapp/src/app/shared/time-picker/index.ts +++ b/services/self-service/src/main/resources/webapp/src/app/shared/time-picker/index.ts @@ -33,7 +33,7 @@ import {LocalDatePipeModule} from '../../core/pipes/local-date-pipe'; export * from './time-picker.component'; @NgModule({ - imports: [CommonModule, FormsModule, ReactiveFormsModule, MaterialModule, LocalDatePipeModule], + imports: [CommonModule, FormsModule, ReactiveFormsModule, MaterialModule, LocalDatePipeModule], declarations: [TimePickerComponent, TimePickerDialogComponent, TimeCoverComponent, TickerComponent], entryComponents: [TimePickerDialogComponent], exports: [TimePickerComponent] diff --git a/services/self-service/src/main/resources/webapp/src/app/shared/time-picker/ticker.component.ts b/services/self-service/src/main/resources/webapp/src/app/shared/time-picker/ticker.component.ts index 45931f1df..623fc18ea 100644 --- a/services/self-service/src/main/resources/webapp/src/app/shared/time-picker/ticker.component.ts +++ b/services/self-service/src/main/resources/webapp/src/app/shared/time-picker/ticker.component.ts @@ -38,12 +38,12 @@ export interface TimeFormat { <button mat-mini-fab class="ticker-selected"></button> </mat-toolbar> <div *ngFor="let step of steps; let i = index" [class]="getTimeValueClass(step, i)" > - <button - mat-mini-fab + <button + mat-mini-fab [color]="selectedTimePart === step ? color : ''" (click)="changeTimeValue(step)" - > - {{ step }} + > + {{ step }} </button> </div> </div> --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
