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.
This commit is contained in:
Ghostkeeper 2021-10-25 01:56:57 +02:00
parent bca2f36186
commit daf450142b
No known key found for this signature in database
GPG key ID: D2A8871EE34EC59A
2 changed files with 80 additions and 20 deletions

View file

@ -1,10 +1,13 @@
# Copyright (c) 2021 Ultimaker B.V. # Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # 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.CuraApplication import CuraApplication
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To make requests to the Ultimaker API with correct authorization. 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 UM.i18n import i18nCatalog
from typing import List, Optional, TYPE_CHECKING from UM.Logger import Logger
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To request the package list from the API. 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. 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.QtCore import QObject
from PyQt5.QtNetwork import QNetworkReply from PyQt5.QtNetwork import QNetworkReply
catalog = i18nCatalog("cura")
class PackageList(ListModel): class PackageList(ListModel):
""" """
Represents a list of packages to be displayed in the interface. Represents a list of packages to be displayed in the interface.
@ -34,6 +39,7 @@ class PackageList(ListModel):
self._is_loading = True self._is_loading = True
self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance()))
self._request_url = f"{Marketplace.PACKAGES_URL}?limit={self.ITEMS_PER_PAGE}" self._request_url = f"{Marketplace.PACKAGES_URL}?limit={self.ITEMS_PER_PAGE}"
self._error_message = ""
self.addRoleName(self.PackageRole, "package") 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. When the request is done, the list will get updated with the new package models.
""" """
self.setIsLoading(True) self.setIsLoading(True)
self.setErrorMessage("") # Clear any previous errors.
http = HttpRequestManager.getInstance() http = HttpRequestManager.getInstance()
http.get( http.get(
@ -83,6 +90,23 @@ class PackageList(ListModel):
""" """
return self._request_url != "" 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: def _parseResponse(self, reply: "QNetworkReply") -> None:
""" """
Parse the response from the package list API request. Parse the response from the package list API request.
@ -92,7 +116,9 @@ class PackageList(ListModel):
""" """
response_data = HttpRequestManager.readJSON(reply) response_data = HttpRequestManager.readJSON(reply)
if "data" not in response_data or "links" not in response_data: 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"]: for package_data in response_data["data"]:
package = PackageModel(package_data, parent = self) 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 reply: The reply with packages. This will most likely be incomplete and should be ignored.
:param error: The error status of the request. :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."))

View file

@ -4,7 +4,7 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import Cura 1.7 as Cura import Cura 1.7 as Cura
import UM 1.0 as UM import UM 1.4 as UM
ScrollView ScrollView
{ {
@ -49,7 +49,7 @@ ScrollView
width: parent.width width: parent.width
height: UM.Theme.getSize("card").height 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. onClicked: pluginList.request() //Load next page in plug-in list.
background: Rectangle background: Rectangle
@ -67,6 +67,26 @@ ScrollView
states: 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 State
{ {
name: "Loading" name: "Loading"
@ -102,12 +122,24 @@ ScrollView
} }
] ]
Item
{
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
UM.StatusIcon
{
id: errorIcon
anchors.fill: parent
status: UM.StatusIcon.Status.ERROR
visible: false
}
UM.RecolorImage UM.RecolorImage
{ {
id: loadMoreIcon id: loadMoreIcon
width: visible ? UM.Theme.getSize("small_button_icon").width : 0 anchors.fill: parent
height: UM.Theme.getSize("small_button_icon").height
anchors.verticalCenter: loadMoreLabel.verticalCenter
source: UM.Theme.getIcon("ArrowDown") source: UM.Theme.getIcon("ArrowDown")
color: UM.Theme.getColor("secondary_button_text") color: UM.Theme.getColor("secondary_button_text")
@ -123,6 +155,7 @@ ScrollView
alwaysRunToEnd: true alwaysRunToEnd: true
} }
} }
}
Label Label
{ {
id: loadMoreLabel id: loadMoreLabel