From daf450142b95958e544e0810d9d0d630686f1b38 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 25 Oct 2021 01:56:57 +0200 Subject: [PATCH] Implement error handling and showing error state If an error occurs, the error message is stored in the list model, so that it can be shown with the list. Contributes to issue CURA-8556. --- plugins/Marketplace/PackageList.py | 35 ++++++++-- plugins/Marketplace/resources/qml/Plugins.qml | 65 ++++++++++++++----- 2 files changed, 80 insertions(+), 20 deletions(-) diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py index 53d3c5909f..f4bb0374ba 100644 --- a/plugins/Marketplace/PackageList.py +++ b/plugins/Marketplace/PackageList.py @@ -1,10 +1,13 @@ # Copyright (c) 2021 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt +from typing import Optional, TYPE_CHECKING + from cura.CuraApplication import CuraApplication from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To make requests to the Ultimaker API with correct authorization. -from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt -from typing import List, Optional, TYPE_CHECKING +from UM.i18n import i18nCatalog +from UM.Logger import Logger from UM.Qt.ListModel import ListModel from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To request the package list from the API. from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope # To request JSON responses from the API. @@ -16,6 +19,8 @@ if TYPE_CHECKING: from PyQt5.QtCore import QObject from PyQt5.QtNetwork import QNetworkReply +catalog = i18nCatalog("cura") + class PackageList(ListModel): """ Represents a list of packages to be displayed in the interface. @@ -34,6 +39,7 @@ class PackageList(ListModel): self._is_loading = True self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) self._request_url = f"{Marketplace.PACKAGES_URL}?limit={self.ITEMS_PER_PAGE}" + self._error_message = "" self.addRoleName(self.PackageRole, "package") @@ -47,6 +53,7 @@ class PackageList(ListModel): When the request is done, the list will get updated with the new package models. """ self.setIsLoading(True) + self.setErrorMessage("") # Clear any previous errors. http = HttpRequestManager.getInstance() http.get( @@ -83,6 +90,23 @@ class PackageList(ListModel): """ return self._request_url != "" + def setErrorMessage(self, error_message: str) -> None: + if(self._error_message != error_message): + self._error_message = error_message + self.errorMessageChanged.emit() + + errorMessageChanged = pyqtSignal() + + @pyqtProperty(str, notify = errorMessageChanged, fset = setErrorMessage) + def errorMessage(self) -> str: + """ + If an error occurred getting the list of packages, an error message will be held here. + + If no error occurred (yet), this will be an empty string. + :return: An error message, if any, or an empty string if everything went okay. + """ + return self._error_message + def _parseResponse(self, reply: "QNetworkReply") -> None: """ Parse the response from the package list API request. @@ -92,7 +116,9 @@ class PackageList(ListModel): """ response_data = HttpRequestManager.readJSON(reply) if "data" not in response_data or "links" not in response_data: - return # TODO: Handle invalid response. + Logger.error(f"Could not interpret the server's response. Missing 'data' or 'links' from response data. Keys in response: {response_data.keys()}") + self.setErrorMessage(catalog.i18nc("@info:error", "Could not interpret the server's response.")) + return for package_data in response_data["data"]: package = PackageModel(package_data, parent = self) @@ -108,4 +134,5 @@ class PackageList(ListModel): :param reply: The reply with packages. This will most likely be incomplete and should be ignored. :param error: The error status of the request. """ - pass # TODO: Handle errors. + Logger.error(f"Could not reach Marketplace server.") + self.setErrorMessage(catalog.i18nc("@info:error", "Could not reach Marketplace.")) diff --git a/plugins/Marketplace/resources/qml/Plugins.qml b/plugins/Marketplace/resources/qml/Plugins.qml index 46e0cf96e5..4fee56f2fb 100644 --- a/plugins/Marketplace/resources/qml/Plugins.qml +++ b/plugins/Marketplace/resources/qml/Plugins.qml @@ -4,7 +4,7 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import Cura 1.7 as Cura -import UM 1.0 as UM +import UM 1.4 as UM ScrollView { @@ -49,7 +49,7 @@ ScrollView width: parent.width height: UM.Theme.getSize("card").height - enabled: pluginList.hasMore && !pluginList.isLoading + enabled: pluginList.hasMore && !pluginList.isLoading || pluginList.errorMessage != "" onClicked: pluginList.request() //Load next page in plug-in list. background: Rectangle @@ -67,6 +67,26 @@ ScrollView states: [ + State + { + name: "Error" + when: pluginList.errorMessage != "" + PropertyChanges + { + target: errorIcon + visible: true + } + PropertyChanges + { + target: loadMoreIcon + visible: false + } + PropertyChanges + { + target: loadMoreLabel + text: catalog.i18nc("@button", "Failed to load plug-ins:") + " " + pluginList.errorMessage + "\n" + catalog.i18nc("@button", "Retry?") + } + }, State { name: "Loading" @@ -102,25 +122,38 @@ ScrollView } ] - UM.RecolorImage + Item { - id: loadMoreIcon - width: visible ? UM.Theme.getSize("small_button_icon").width : 0 + width: (errorIcon.visible || loadMoreIcon.visible) ? UM.Theme.getSize("small_button_icon").width : 0 height: UM.Theme.getSize("small_button_icon").height anchors.verticalCenter: loadMoreLabel.verticalCenter - source: UM.Theme.getIcon("ArrowDown") - color: UM.Theme.getColor("secondary_button_text") - - RotationAnimator + UM.StatusIcon { - target: loadMoreIcon - from: 0 - to: 360 - duration: 1000 - loops: Animation.Infinite - running: pluginList.isLoading - alwaysRunToEnd: true + id: errorIcon + anchors.fill: parent + + status: UM.StatusIcon.Status.ERROR + visible: false + } + UM.RecolorImage + { + id: loadMoreIcon + anchors.fill: parent + + source: UM.Theme.getIcon("ArrowDown") + color: UM.Theme.getColor("secondary_button_text") + + RotationAnimator + { + target: loadMoreIcon + from: 0 + to: 360 + duration: 1000 + loops: Animation.Infinite + running: pluginList.isLoading + alwaysRunToEnd: true + } } } Label