From ac8ee00affc48fe78210a1af1dbbba08f276dc0a Mon Sep 17 00:00:00 2001 From: Vincent Liu <5900509+onecent1101@users.noreply.github.com> Date: Wed, 9 Jun 2021 19:29:34 -0700 Subject: [PATCH] [LYN-4288] Adding error page if resource mapping tool has invalid setup (#1219) --- .../controller/error_controller.py | 33 +++++++ .../manager/configuration_manager.py | 11 ++- .../manager/controller_manager.py | 26 ++++-- .../manager/thread_manager.py | 11 ++- .../manager/view_manager.py | 30 +++++-- .../model/error_messages.py | 6 ++ .../model/notification_label_text.py | 2 + .../model/view_size_constants.py | 12 +++ .../resource_mapping_tool.py | 20 ++--- .../style/base_style_sheet.qss | 15 ++++ .../manager/test_configuration_manager.py | 15 +++- .../unit/manager/test_controller_manager.py | 11 ++- .../tests/unit/manager/test_thread_manager.py | 8 +- .../tests/unit/manager/test_view_manager.py | 35 +++++++- .../view/common_view_components.py | 4 +- .../ResourceMappingTool/view/error_page.py | 89 +++++++++++++++++++ 16 files changed, 281 insertions(+), 47 deletions(-) create mode 100644 Gems/AWSCore/Code/Tools/ResourceMappingTool/controller/error_controller.py create mode 100644 Gems/AWSCore/Code/Tools/ResourceMappingTool/view/error_page.py diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/controller/error_controller.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/controller/error_controller.py new file mode 100644 index 0000000000..244245ad57 --- /dev/null +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/controller/error_controller.py @@ -0,0 +1,33 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + +from PySide2.QtCore import (QCoreApplication, QObject) + +from manager.view_manager import ViewManager +from view.error_page import ErrorPage + + +class ErrorController(QObject): + """ + ErrorPage Controller + """ + def __init__(self) -> None: + super(ErrorController, self).__init__() + # Initialize manager references + self._view_manager: ViewManager = ViewManager.get_instance() + # Initialize view references + self._error_page: ErrorPage = self._view_manager.get_error_page() + + def _ok(self) -> None: + QCoreApplication.instance().quit() + + def setup(self) -> None: + self._error_page.ok_button.clicked.connect(self._ok) diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/manager/configuration_manager.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/manager/configuration_manager.py index fc188582c7..7c5ceb6946 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/manager/configuration_manager.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/manager/configuration_manager.py @@ -48,11 +48,15 @@ class ConfigurationManager(object): def configuration(self, new_configuration: ConfigurationManager) -> None: self._configuration = new_configuration - def setup(self, config_path: str) -> bool: + def setup(self, profile_name: str, config_path: str) -> bool: result: bool = True - logger.info("Setting up default configuration ...") + logger.debug("Setting up default configuration ...") try: - normalized_config_path: str = file_utils.normalize_file_path(config_path); + logger.debug("Setting up boto3 default session ...") + aws_utils.setup_default_session(profile_name) + + logger.debug("Setting up config directory and files ...") + normalized_config_path: str = file_utils.normalize_file_path(config_path) if normalized_config_path: self._configuration.config_directory = normalized_config_path else: @@ -61,6 +65,7 @@ class ConfigurationManager(object): file_utils.find_files_with_suffix_under_directory(self._configuration.config_directory, constants.RESOURCE_MAPPING_CONFIG_FILE_NAME_SUFFIX) + logger.debug("Setting up aws account id and region ...") self._configuration.account_id = aws_utils.get_default_account_id() self._configuration.region = aws_utils.get_default_region() except (RuntimeError, FileNotFoundError) as e: diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/manager/controller_manager.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/manager/controller_manager.py index 48c45d0350..61eaaddea2 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/manager/controller_manager.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/manager/controller_manager.py @@ -12,6 +12,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. from __future__ import annotations import logging +from controller.error_controller import ErrorController from controller.import_resources_controller import ImportResourcesController from controller.view_edit_controller import ViewEditController from model import error_messages @@ -33,8 +34,9 @@ class ControllerManager(object): def __init__(self) -> None: if ControllerManager.__instance is None: - self._view_edit_controller: ViewEditController = ViewEditController() - self._import_resources_controller: ImportResourcesController = ImportResourcesController() + self._error_controller: ErrorController = None + self._view_edit_controller: ViewEditController = None + self._import_resources_controller: ImportResourcesController = None ControllerManager.__instance = self else: raise AssertionError(error_messages.SINGLETON_OBJECT_ERROR_MESSAGE.format("ControllerManager")) @@ -47,9 +49,17 @@ class ControllerManager(object): def view_edit_controller(self) -> ViewEditController: return self._view_edit_controller - def setup(self) -> None: - logger.info("Setting up ViewEdit and ImportResource controllers ...") - self._view_edit_controller.setup() - self._import_resources_controller.setup() - self._import_resources_controller.add_import_resources_sender.connect( - self._view_edit_controller.add_import_resources_receiver) + def setup(self, setup_error: bool) -> None: + if setup_error: + logger.debug("Setting up Error controllers ...") + self._error_controller = ErrorController() + self._error_controller.setup() + else: + logger.debug("Setting up ViewEdit and ImportResource controllers ...") + self._view_edit_controller = ViewEditController() + self._import_resources_controller = ImportResourcesController() + + self._view_edit_controller.setup() + self._import_resources_controller.setup() + self._import_resources_controller.add_import_resources_sender.connect( + self._view_edit_controller.add_import_resources_receiver) diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/manager/thread_manager.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/manager/thread_manager.py index 516a085439..4de928bb1b 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/manager/thread_manager.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/manager/thread_manager.py @@ -37,10 +37,13 @@ class ThreadManager(object): else: raise AssertionError(error_messages.SINGLETON_OBJECT_ERROR_MESSAGE.format("ThreadManager")) - def setup(self, thread_count: int = 1) -> None: - # Based on prototype use case, we just need 1 thread - logger.info(f"Setting up thread pool with MaxThreadCount={thread_count} ...") - self._thread_pool.setMaxThreadCount(thread_count) + def setup(self, setup_error: bool, thread_count: int = 1) -> None: + if setup_error: + logger.debug("Skip thread pool creation, as there is major setup error.") + else: + # Based on prototype use case, we just need 1 thread + logger.debug(f"Setting up thread pool with MaxThreadCount={thread_count} ...") + self._thread_pool.setMaxThreadCount(thread_count) """Reserves a thread and uses it to run runnable worker, unless this thread will make the current thread count exceed max thread count. In that case, runnable is added to a run queue instead.""" diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/manager/view_manager.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/manager/view_manager.py index fff5c4b59a..46158afb39 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/manager/view_manager.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/manager/view_manager.py @@ -16,6 +16,7 @@ from PySide2.QtGui import QIcon from PySide2.QtWidgets import (QMainWindow, QStackedWidget, QWidget) from model import (error_messages, view_size_constants) +from view.error_page import ErrorPage from view.import_resources_page import ImportResourcesPage from view.view_edit_page import ViewEditPage @@ -27,6 +28,10 @@ class ViewManagerConstants(object): IMPORT_RESOURCES_PAGE_INDEX: int = 1 +# Error page will be a single page +ERROR_PAGE_INDEX: int = 0 + + class ViewManager(object): """ View manager maintains the main stacked pages for this tool, which @@ -58,21 +63,32 @@ class ViewManager(object): ViewManager.__instance = self else: raise AssertionError(error_messages.SINGLETON_OBJECT_ERROR_MESSAGE.format("ViewManager")) - + + def get_error_page(self) -> QWidget: + return self._resource_mapping_stacked_pages.widget(ERROR_PAGE_INDEX) + def get_view_edit_page(self) -> QWidget: return self._resource_mapping_stacked_pages.widget(ViewManagerConstants.VIEW_AND_EDIT_PAGE_INDEX) def get_import_resources_page(self) -> QWidget: return self._resource_mapping_stacked_pages.widget(ViewManagerConstants.IMPORT_RESOURCES_PAGE_INDEX) - def setup(self) -> None: - logger.debug("Setting up ViewEdit and ImportResources view pages ...") - self._resource_mapping_stacked_pages.addWidget(ViewEditPage()) - self._resource_mapping_stacked_pages.addWidget(ImportResourcesPage()) + def setup(self, setup_error: bool) -> None: + if setup_error: + logger.debug("Setting up Error view pages ...") + self._resource_mapping_stacked_pages.addWidget(ErrorPage()) + self._main_window.adjustSize() # fit error page size + else: + logger.debug("Setting up ViewEdit and ImportResources view pages ...") + self._resource_mapping_stacked_pages.addWidget(ViewEditPage()) + self._resource_mapping_stacked_pages.addWidget(ImportResourcesPage()) - def show(self) -> None: + def show(self, setup_error: bool) -> None: """Show up the tool view by setting default page index and showing main widget""" - self._resource_mapping_stacked_pages.setCurrentIndex(ViewManagerConstants.VIEW_AND_EDIT_PAGE_INDEX) + if setup_error: + self._resource_mapping_stacked_pages.setCurrentIndex(ERROR_PAGE_INDEX) + else: + self._resource_mapping_stacked_pages.setCurrentIndex(ViewManagerConstants.VIEW_AND_EDIT_PAGE_INDEX) self._main_window.show() def switch_to_view_edit_page(self) -> None: diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/model/error_messages.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/model/error_messages.py index 08ab2a146d..7aef2068a2 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/model/error_messages.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/model/error_messages.py @@ -9,6 +9,12 @@ remove or modify any license notices. This file is distributed on an "AS IS" BAS WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. """ +ERROR_PAGE_TOOL_SETUP_ERROR_MESSAGE: str = \ + "AWS credentials are missing or invalid. See " \ + ""\ + "documentation for details." \ + "
Check log file under Gems/AWSCore/Code/Tool/ResourceMappingTool for further information." + VIEW_EDIT_PAGE_SAVING_FAILED_WITH_INVALID_ROW_ERROR_MESSAGE: str = \ "Row {} have errors. Please correct errors or delete the row to proceed." VIEW_EDIT_PAGE_READ_FROM_JSON_FAILED_WITH_UNEXPECTED_FILE_ERROR_MESSAGE: str = \ diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/model/notification_label_text.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/model/notification_label_text.py index 1819e4c4ce..aecff69fb1 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/model/notification_label_text.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/model/notification_label_text.py @@ -11,6 +11,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. NOTIFICATION_LOADING_MESSAGE: str = "Loading..." +ERROR_PAGE_OK_TEXT: str = "OK" + VIEW_EDIT_PAGE_CONFIG_FILE_TEXT: str = "Config File" VIEW_EDIT_PAGE_CONFIG_LOCATION_TEXT: str = "Config Location:" VIEW_EDIT_PAGE_ADD_ROW_TEXT: str = "Add Row" diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/model/view_size_constants.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/model/view_size_constants.py index c01d81b75c..7c0eb55885 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/model/view_size_constants.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/model/view_size_constants.py @@ -17,6 +17,18 @@ MAIN_PAGE_LAYOUT_MARGIN_TOPBOTTOM: int = 15 INTERACTION_COMPONENT_HEIGHT: int = 25 +"""error page related constants""" +ERROR_PAGE_LAYOUT_MARGIN_LEFTRIGHT: int = 10 +ERROR_PAGE_LAYOUT_MARGIN_TOPBOTTOM: int = 10 + +ERROR_PAGE_MAIN_WINDOW_WIDTH: int = 600 +ERROR_PAGE_MAIN_WINDOW_HEIGHT: int = 145 + +ERROR_PAGE_NOTIFICATION_AREA_HEIGHT: int = 100 +ERROR_PAGE_FOOTER_AREA_HEIGHT: int = 45 + +OK_BUTTON_WIDTH: int = 90 + """view edit page related constants""" VIEW_EDIT_PAGE_HEADER_AREA_HEIGHT: int = 65 VIEW_EDIT_PAGE_CENTER_AREA_HEIGHT: int = 500 diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/resource_mapping_tool.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/resource_mapping_tool.py index 6a56491b24..177dae349e 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/resource_mapping_tool.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/resource_mapping_tool.py @@ -73,32 +73,22 @@ if __name__ == "__main__": except FileNotFoundError: logger.warning("Failed to load style sheet for resource mapping tool") - logger.info("Initializing boto3 default session ...") - try: - aws_utils.setup_default_session(arguments.profile) - except RuntimeError as error: - logger.error(error) - environment_utils.cleanup_qt_environment() - exit(-1) - logger.info("Initializing configuration manager ...") configuration_manager: ConfigurationManager = ConfigurationManager() - if not configuration_manager.setup(arguments.config_path): - environment_utils.cleanup_qt_environment() - exit(-1) + configuration_error: bool = not configuration_manager.setup(arguments.profile, arguments.config_path) logger.info("Initializing thread manager ...") thread_manager: ThreadManager = ThreadManager() - thread_manager.setup() + thread_manager.setup(configuration_error) logger.info("Initializing view manager ...") view_manager: ViewManager = ViewManager() - view_manager.setup() + view_manager.setup(configuration_error) logger.info("Initializing controller manager ...") controller_manager: ControllerManager = ControllerManager() - controller_manager.setup() + controller_manager.setup(configuration_error) - view_manager.show() + view_manager.show(configuration_error) sys.exit(app.exec_()) diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/style/base_style_sheet.qss b/Gems/AWSCore/Code/Tools/ResourceMappingTool/style/base_style_sheet.qss index c23c3d90bd..2638052e2c 100644 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/style/base_style_sheet.qss +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/style/base_style_sheet.qss @@ -368,3 +368,18 @@ QFrame#NotificationFrame margin: 4px; padding: 4px; } + +QFrame#ErrorPage +{ + background-color: #2d2d2d; + border: 1px solid #4A90E2; + border-radius: 2px; + margin: 0px; + padding: 15px; +} + +QFrame#ErrorPage QLabel#NotificationIcon +{ + padding-left: 15px; + padding-right: 15px; +} diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_configuration_manager.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_configuration_manager.py index 552f9fff01..7c6ee496a0 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_configuration_manager.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_configuration_manager.py @@ -33,6 +33,7 @@ class TestConfigurationManager(TestCase): def test_get_instance_raise_exception(self) -> None: self.assertRaises(Exception, ConfigurationManager) + @patch("utils.aws_utils.setup_default_session") @patch("utils.aws_utils.get_default_region", return_value=_expected_region) @patch("utils.aws_utils.get_default_account_id", return_value=_expected_account_id) @patch("utils.file_utils.find_files_with_suffix_under_directory", return_value=_expected_config_files) @@ -42,8 +43,10 @@ class TestConfigurationManager(TestCase): mock_check_path_exists: MagicMock, mock_find_files_with_suffix_under_directory: MagicMock, mock_get_default_account_id: MagicMock, - mock_get_default_region: MagicMock) -> None: - TestConfigurationManager._expected_configuration_manager.setup("") + mock_get_default_region: MagicMock, + mock_setup_default_session: MagicMock) -> None: + TestConfigurationManager._expected_configuration_manager.setup("", "") + mock_setup_default_session.assert_called_once() mock_get_current_directory_path.assert_called_once() mock_check_path_exists.assert_called_once_with(TestConfigurationManager._expected_directory_path) mock_find_files_with_suffix_under_directory.assert_called_once_with( @@ -59,6 +62,7 @@ class TestConfigurationManager(TestCase): assert TestConfigurationManager._expected_configuration_manager.configuration.region == \ TestConfigurationManager._expected_region + @patch("utils.aws_utils.setup_default_session") @patch("utils.aws_utils.get_default_region", return_value=_expected_region) @patch("utils.aws_utils.get_default_account_id", return_value=_expected_account_id) @patch("utils.file_utils.find_files_with_suffix_under_directory", return_value=_expected_config_files) @@ -68,8 +72,11 @@ class TestConfigurationManager(TestCase): mock_check_path_exists: MagicMock, mock_find_files_with_suffix_under_directory: MagicMock, mock_get_default_account_id: MagicMock, - mock_get_default_region: MagicMock) -> None: - TestConfigurationManager._expected_configuration_manager.setup(TestConfigurationManager._expected_directory_path) + mock_get_default_region: MagicMock, + mock_setup_default_session: MagicMock) -> None: + TestConfigurationManager._expected_configuration_manager.setup( + "", TestConfigurationManager._expected_directory_path) + mock_setup_default_session.assert_called_once() mock_normalize_file_path.assert_called_once() mock_check_path_exists.assert_called_once_with(TestConfigurationManager._expected_directory_path) mock_find_files_with_suffix_under_directory.assert_called_once_with( diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_controller_manager.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_controller_manager.py index 93e49415ca..7f2f1dd3f2 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_controller_manager.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_controller_manager.py @@ -25,6 +25,9 @@ class TestControllerManager(TestCase): @classmethod def setUpClass(cls) -> None: + error_controller_patcher: patch = patch("manager.controller_manager.ErrorController") + cls._mock_error_controller = error_controller_patcher.start() + import_resources_controller_patcher: patch = patch("manager.controller_manager.ImportResourcesController") cls._mock_import_resources_controller = import_resources_controller_patcher.start() @@ -52,8 +55,14 @@ class TestControllerManager(TestCase): mocked_import_resources_controller: MagicMock = \ TestControllerManager._mock_import_resources_controller.return_value - TestControllerManager._expected_controller_manager.setup() + TestControllerManager._expected_controller_manager.setup(False) mocked_view_edit_controller.setup.assert_called_once() mocked_import_resources_controller.setup.assert_called_once() mocked_import_resources_controller.add_import_resources_sender.connect.assert_called_once_with( mocked_view_edit_controller.add_import_resources_receiver) + + def test_setup_error_controller_setup_gets_invoked(self) -> None: + mocked_error_controller: MagicMock = TestControllerManager._mock_error_controller.return_value + + TestControllerManager._expected_controller_manager.setup(True) + mocked_error_controller.setup.assert_called_once() diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_thread_manager.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_thread_manager.py index 456a17efe2..06e2b4abbd 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_thread_manager.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_thread_manager.py @@ -45,9 +45,15 @@ class TestThreadManager(TestCase): def test_setup_thread_pool_setup_with_expected_configuration(self) -> None: mocked_thread_pool: MagicMock = TestThreadManager._mock_thread_pool.return_value - TestThreadManager._expected_thread_manager.setup() + TestThreadManager._expected_thread_manager.setup(False) mocked_thread_pool.setMaxThreadCount.assert_called_once_with(1) + def test_setup_thread_pool_skip_setup(self) -> None: + mocked_thread_pool: MagicMock = TestThreadManager._mock_thread_pool.return_value + + TestThreadManager._expected_thread_manager.setup(True) + mocked_thread_pool.setMaxThreadCount.asset_not_called() + def test_start_thread_pool_start_expected_worker(self) -> None: mocked_thread_pool: MagicMock = TestThreadManager._mock_thread_pool.return_value expected_mocked_worker: MagicMock = MagicMock() diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_view_manager.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_view_manager.py index e5c84c6579..57ec41ba0a 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_view_manager.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/tests/unit/manager/test_view_manager.py @@ -13,13 +13,14 @@ from typing import List from unittest import TestCase from unittest.mock import (call, MagicMock, patch) -from manager.view_manager import (ViewManager, ViewManagerConstants) +from manager.view_manager import (ERROR_PAGE_INDEX, ViewManager, ViewManagerConstants) class TestViewManager(TestCase): """ ViewManager unit test cases """ + _mock_error_page: MagicMock _mock_import_resources_page: MagicMock _mock_view_edit_page: MagicMock _mock_main_window: MagicMock @@ -28,6 +29,9 @@ class TestViewManager(TestCase): @classmethod def setUpClass(cls) -> None: + error_page_patcher: patch = patch("manager.view_manager.ErrorPage") + cls._mock_error_page = error_page_patcher.start() + import_resources_page_patcher: patch = patch("manager.view_manager.ImportResourcesPage") cls._mock_import_resources_page = import_resources_page_patcher.start() @@ -60,6 +64,15 @@ class TestViewManager(TestCase): def test_get_instance_raise_exception(self) -> None: self.assertRaises(Exception, ViewManager) + def test_get_error_page_return_expected_page(self) -> None: + expected_page: MagicMock = TestViewManager._mock_error_page.return_value + mocked_stacked_pages: MagicMock = TestViewManager._mock_stacked_pages.return_value + mocked_stacked_pages.widget.return_value = expected_page + + actual_page: MagicMock = TestViewManager._expected_view_manager.get_error_page() + mocked_stacked_pages.widget.assert_called_once_with(ERROR_PAGE_INDEX) + assert actual_page == expected_page + def test_get_import_resources_page_return_expected_page(self) -> None: expected_page: MagicMock = TestViewManager._mock_import_resources_page.return_value mocked_stacked_pages: MagicMock = TestViewManager._mock_stacked_pages.return_value @@ -83,18 +96,34 @@ class TestViewManager(TestCase): mocked_import_resources_page: MagicMock = TestViewManager._mock_import_resources_page.return_value mocked_view_edit_page: MagicMock = TestViewManager._mock_view_edit_page.return_value - TestViewManager._expected_view_manager.setup() + TestViewManager._expected_view_manager.setup(False) mocked_calls: List[call] = [call(mocked_view_edit_page), call(mocked_import_resources_page)] mocked_stacked_pages.addWidget.assert_has_calls(mocked_calls) + def test_setup_error_page_only(self) -> None: + mocked_stacked_pages: MagicMock = TestViewManager._mock_stacked_pages.return_value + mocked_error_page: MagicMock = TestViewManager._mock_error_page.return_value + + TestViewManager._expected_view_manager.setup(True) + mocked_calls: List[call] = [call(mocked_error_page)] + mocked_stacked_pages.addWidget.assert_has_calls(mocked_calls) + def test_show_stacked_pages_show_with_expected_index(self) -> None: mocked_stacked_pages: MagicMock = TestViewManager._mock_stacked_pages.return_value mocked_main_window: MagicMock = TestViewManager._mock_main_window.return_value - TestViewManager._expected_view_manager.show() + TestViewManager._expected_view_manager.show(False) mocked_stacked_pages.setCurrentIndex.assert_called_once_with(ViewManagerConstants.VIEW_AND_EDIT_PAGE_INDEX) mocked_main_window.show.assert_called_once() + def test_show_stacked_pages_show_error_plage(self) -> None: + mocked_stacked_pages: MagicMock = TestViewManager._mock_stacked_pages.return_value + mocked_main_window: MagicMock = TestViewManager._mock_main_window.return_value + + TestViewManager._expected_view_manager.show(True) + mocked_stacked_pages.setCurrentIndex.assert_called_once_with(ERROR_PAGE_INDEX) + mocked_main_window.show.assert_called_once() + def test_switch_to_view_edit_page_stacked_pages_switch_to_expected_index(self) -> None: mocked_stacked_pages: MagicMock = TestViewManager._mock_stacked_pages.return_value diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/view/common_view_components.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/view/common_view_components.py index 8aad0a785c..39c94ccf77 100755 --- a/Gems/AWSCore/Code/Tools/ResourceMappingTool/view/common_view_components.py +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/view/common_view_components.py @@ -37,10 +37,12 @@ class NotificationFrame(QFrame): self.setFrameStyle(QFrame.StyledPanel | QFrame.Plain) icon_label: QLabel = QLabel(self) + icon_label.setObjectName("NotificationIcon") icon_label.setPixmap(pixmap) self._title_label: QLabel = QLabel(title, self) - self._title_label.setObjectName("Title") + self._title_label.setOpenExternalLinks(True) + self._title_label.setObjectName("NotificationTitle") self._title_label.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)) self._title_label.setWordWrap(True) diff --git a/Gems/AWSCore/Code/Tools/ResourceMappingTool/view/error_page.py b/Gems/AWSCore/Code/Tools/ResourceMappingTool/view/error_page.py new file mode 100644 index 0000000000..2b42d2bfc2 --- /dev/null +++ b/Gems/AWSCore/Code/Tools/ResourceMappingTool/view/error_page.py @@ -0,0 +1,89 @@ +""" +All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or +its licensors. + +For complete copyright and license terms please see the LICENSE at the root of this +distribution (the "License"). All use of this software is governed by the License, +or, if provided, by the license below or the license accompanying this file. Do not +remove or modify any license notices. This file is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + +from PySide2.QtGui import QPixmap +from PySide2.QtWidgets import (QHBoxLayout, QLayout, QPushButton, QSizePolicy, QSpacerItem, QVBoxLayout, QWidget) + +from model import (error_messages, notification_label_text, view_size_constants) +from view.common_view_components import NotificationFrame + + +class ErrorPage(QWidget): + """ + Error Page + """ + def __init__(self) -> None: + super().__init__() + self.setGeometry(0, 0, + view_size_constants.ERROR_PAGE_MAIN_WINDOW_WIDTH, + view_size_constants.ERROR_PAGE_MAIN_WINDOW_HEIGHT) + + page_vertical_layout: QVBoxLayout = QVBoxLayout(self) + page_vertical_layout.setSizeConstraint(QLayout.SetMinimumSize) + page_vertical_layout.setMargin(0) + + self._setup_notification_area() + page_vertical_layout.addWidget(self._notification_area) + + self._setup_footer_area() + page_vertical_layout.addWidget(self._footer_area) + + def _setup_notification_area(self) -> None: + self._notification_area: QWidget = QWidget(self) + self._notification_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) + self._notification_area.setMinimumSize(view_size_constants.ERROR_PAGE_MAIN_WINDOW_WIDTH, + view_size_constants.ERROR_PAGE_NOTIFICATION_AREA_HEIGHT) + + notification_area_layout: QVBoxLayout = QVBoxLayout(self._notification_area) + notification_area_layout.setSizeConstraint(QLayout.SetMinimumSize) + notification_area_layout.setContentsMargins( + view_size_constants.ERROR_PAGE_LAYOUT_MARGIN_LEFTRIGHT, + view_size_constants.MAIN_PAGE_LAYOUT_MARGIN_TOPBOTTOM, + view_size_constants.ERROR_PAGE_LAYOUT_MARGIN_LEFTRIGHT, 0) + + notification_frame: NotificationFrame = \ + NotificationFrame(self, QPixmap(":/error_report_warning.svg"), + error_messages.ERROR_PAGE_TOOL_SETUP_ERROR_MESSAGE, False) + notification_frame.setObjectName("ErrorPage") + notification_frame.setMinimumSize(view_size_constants.ERROR_PAGE_MAIN_WINDOW_WIDTH, + view_size_constants.ERROR_PAGE_NOTIFICATION_AREA_HEIGHT) + notification_frame.setVisible(True) + notification_area_layout.addWidget(notification_frame) + + def _setup_footer_area(self) -> None: + self._footer_area: QWidget = QWidget(self) + self._footer_area.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) + self._footer_area.setMaximumSize(view_size_constants.TOOL_APPLICATION_MAIN_WINDOW_WIDTH, + view_size_constants.ERROR_PAGE_FOOTER_AREA_HEIGHT) + + footer_area_layout: QHBoxLayout = QHBoxLayout(self._footer_area) + footer_area_layout.setSizeConstraint(QLayout.SetMinimumSize) + footer_area_layout.setContentsMargins( + view_size_constants.ERROR_PAGE_LAYOUT_MARGIN_LEFTRIGHT, + view_size_constants.ERROR_PAGE_LAYOUT_MARGIN_TOPBOTTOM, + view_size_constants.ERROR_PAGE_LAYOUT_MARGIN_LEFTRIGHT, + view_size_constants.ERROR_PAGE_LAYOUT_MARGIN_TOPBOTTOM) + + footer_area_spacer: QSpacerItem = QSpacerItem(view_size_constants.ERROR_PAGE_MAIN_WINDOW_WIDTH, + view_size_constants.INTERACTION_COMPONENT_HEIGHT, + QSizePolicy.MinimumExpanding, QSizePolicy.Minimum) + footer_area_layout.addItem(footer_area_spacer) + + self._ok_button: QPushButton = QPushButton(self._footer_area) + self._ok_button.setObjectName("Secondary") + self._ok_button.setText(notification_label_text.ERROR_PAGE_OK_TEXT) + self._ok_button.setMinimumSize(view_size_constants.OK_BUTTON_WIDTH, + view_size_constants.INTERACTION_COMPONENT_HEIGHT) + footer_area_layout.addWidget(self._ok_button) + + @property + def ok_button(self) -> QPushButton: + return self._ok_button