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