Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved Error Dialog #2953

Merged
merged 15 commits into from
Jun 16, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 3 additions & 6 deletions src/main/java/org/cryptomator/common/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,15 @@ private ErrorCode(Throwable throwable, Throwable rootCause, int rootCauseSpecifi
this.rootCauseSpecificFrames = rootCauseSpecificFrames;
}

// visible for testing
String methodCode() {
public String methodCode() {
return format(traceCode(rootCause, LATEST_FRAME));
}

// visible for testing
String rootCauseCode() {
public String rootCauseCode() {
return format(traceCode(rootCause, rootCauseSpecificFrames));
}

// visible for testing
String throwableCode() {
public String throwableCode() {
return format(traceCode(throwable, ALL_FRAMES));
}

Expand Down
182 changes: 175 additions & 7 deletions src/main/java/org/cryptomator/ui/error/ErrorController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.cryptomator.ui.error;

import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import org.cryptomator.common.Environment;
import org.cryptomator.common.ErrorCode;
import org.cryptomator.common.Nullable;
Expand All @@ -16,13 +18,24 @@
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.stage.Stage;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Comparator;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;

public class ErrorController implements FxController {

private static final String ERROR_CODES_URL = "https://gist.githubusercontent.com/cryptobot/accba9fb9555e7192271b85606f97230/raw/errorcodes.json";
private static final String SEARCH_URL_FORMAT = "https://github.com/cryptomator/cryptomator/discussions/categories/errors?discussions_q=category:Errors+%s";
private static final String REPORT_URL_FORMAT = "https://github.com/cryptomator/cryptomator/discussions/new?category=Errors&title=Error+%s&body=%s";
private static final String SEARCH_ERRORCODE_DELIM = " OR ";
Expand All @@ -46,16 +59,28 @@ public class ErrorController implements FxController {
private final Stage window;
private final Environment environment;

private BooleanProperty copiedDetails = new SimpleBooleanProperty();
private final BooleanProperty copiedDetails = new SimpleBooleanProperty();
private final BooleanProperty errorSolutionFound = new SimpleBooleanProperty();
mindmonk marked this conversation as resolved.
Show resolved Hide resolved
private final BooleanProperty isLoadingHttpResponse = new SimpleBooleanProperty();
private ErrorDiscussion matchingErrorDiscussion;

@Inject
ErrorController(Application application, @Named("stackTrace") String stackTrace, ErrorCode errorCode, @Nullable Scene previousScene, Stage window, Environment environment) {
ErrorController(Application application, @Named("stackTrace") String stackTrace, ErrorCode errorCode, @Nullable Scene previousScene, Stage window, Environment environment, ExecutorService executorService) {
this.application = application;
this.stackTrace = stackTrace;
this.errorCode = errorCode;
this.previousScene = previousScene;
this.window = window;
this.environment = environment;

isLoadingHttpResponse.set(true);
HttpClient httpClient = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
HttpRequest httpRequest = HttpRequest.newBuilder()
.uri(URI.create(ERROR_CODES_URL))
.build();
httpClient.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofInputStream())
.thenAcceptAsync(this::loadHttpResponse, executorService)
.whenCompleteAsync((r,e) -> isLoadingHttpResponse.set(false), Platform::runLater);
}

@FXML
Expand All @@ -70,6 +95,13 @@ public void close() {
window.close();
}

@FXML
public void showSolution() {
if(matchingErrorDiscussion != null){
application.getHostServices().showDocument(matchingErrorDiscussion.url);
}
}

@FXML
public void searchError() {
var searchTerm = URLEncoder.encode(getErrorCode().replace(ErrorCode.DELIM, SEARCH_ERRORCODE_DELIM), StandardCharsets.UTF_8);
Expand All @@ -95,13 +127,132 @@ public void copyDetails() {
Clipboard.getSystemClipboard().setContent(clipboardContent);

copiedDetails.set(true);
CompletableFuture.delayedExecutor(2, TimeUnit.SECONDS, Platform::runLater).execute(() -> {
copiedDetails.set(false);
});
CompletableFuture.delayedExecutor(2, TimeUnit.SECONDS, Platform::runLater).execute(() -> copiedDetails.set(false));
}

/* Getter/Setter */
private void loadHttpResponse(HttpResponse<InputStream> response){
if (response.statusCode() == 200) {
Map<String,ErrorDiscussion> errorDiscussionMap = new Gson().fromJson(
new InputStreamReader(response.body(),StandardCharsets.UTF_8),
new TypeToken<Map<String,ErrorDiscussion>>(){}.getType());

if(errorDiscussionMap.values().stream().anyMatch(this::isPartialMatchFilter)) {
Comparator<ErrorDiscussion> comp = this::compareExactMatch;
Optional<ErrorDiscussion> value = errorDiscussionMap.values().stream().min(comp
.thenComparing(this::compareSecondLevelMatch)
.thenComparing(this::compareThirdLevelMatch)
.thenComparing(this::compareIsAnswered)
.thenComparing(this::compareUpvoteCount));

if(value.isPresent()){
matchingErrorDiscussion = value.get();
errorSolutionFound.set(true);
}
}
}
}

public boolean isPartialMatchFilter(ErrorDiscussion errorDiscussion) {
mindmonk marked this conversation as resolved.
Show resolved Hide resolved
return errorDiscussion.title.contains(" " + errorCode.methodCode());
}

/**
* Compares two ErrorDiscussion objects based on their upvote counts and returns the result.
*
* @param ed1 The first ErrorDiscussion object.
* @param ed2 The second ErrorDiscussion object.
* @return An integer indicating which ErrorDiscussion object has a higher upvote count:
* - A positive value if ed2 has a higher upvote count than ed1,
* - A negative value if ed1 has a higher upvote count than ed2,
* - Or 0 if both upvote counts are equal.
*/
public int compareUpvoteCount(ErrorDiscussion ed1, ErrorDiscussion ed2) {
return Integer.compare(ed2.upvoteCount, ed1.upvoteCount);
}
mindmonk marked this conversation as resolved.
Show resolved Hide resolved

/**
* Compares two ErrorDiscussion objects based on their answered status and returns the result.
*
* @param ed1 The first ErrorDiscussion object.
* @param ed2 The second ErrorDiscussion object.
* @return An integer indicating the answered status of the ErrorDiscussion objects:
* - A negative value (-1) if ed1 is considered answered and ed2 is considered unanswered,
* - A positive value (1) if ed1 is considered unanswered and ed2 is considered answered,
* - Or 0 if both ErrorDiscussion objects are considered answered or unanswered.
*/
public int compareIsAnswered(ErrorDiscussion ed1, ErrorDiscussion ed2) {
if (ed1.answer!=null && ed2.answer==null) {
return -1;
} else if (ed1.answer==null && ed2.answer!=null) {
return 1;
} else {
return 0;
}
}
mindmonk marked this conversation as resolved.
Show resolved Hide resolved

/**
* Compares two ErrorDiscussion objects based on the presence of an exact match with the error code in their titles and returns the result.
*
* @param ed1 The first ErrorDiscussion object.
* @param ed2 The second ErrorDiscussion object.
* @return An integer indicating the comparison result based on the presence of an exact match with the error code in the titles:
* - A negative value (-1) if ed1 has an exact match with the error code in the title and ed2 does not have a match,
* - A positive value (1) if ed1 does not have a match and ed2 has an exact match with the error code in the title,
* - Or 0 if both ErrorDiscussion objects either have an exact match or do not have a match with the error code in the titles.
*/
public int compareExactMatch(ErrorDiscussion ed1, ErrorDiscussion ed2) {
mindmonk marked this conversation as resolved.
Show resolved Hide resolved
if (ed1.title.contains(getErrorCode()) && !ed2.title.contains(getErrorCode())) {
return -1;
} else if (!ed1.title.contains(getErrorCode()) && ed2.title.contains(getErrorCode())) {
return 1;
} else {
return 0;
}
}

/**
* Compares two ErrorDiscussion objects based on the presence of a second-level match with the error code in their titles and returns the result.
*
* @param ed1 The first ErrorDiscussion object.
* @param ed2 The second ErrorDiscussion object.
* @return An integer indicating the comparison result based on the presence of a second-level match with the error code in the titles:
* - A negative value (-1) if ed1 has a second-level match with the error code in the title and ed2 does not have a match,
* - A positive value (1) if ed1 does not have a match and ed2 has a second-level match with the error code in the title,
* - Or 0 if both ErrorDiscussion objects either have a second-level match or do not have a match with the error code in the titles.
*/
public int compareSecondLevelMatch(ErrorDiscussion ed1, ErrorDiscussion ed2) {
mindmonk marked this conversation as resolved.
Show resolved Hide resolved
String value = " " + errorCode.methodCode() + ErrorCode.DELIM + errorCode.rootCauseCode();
if (ed1.title.contains(value) && !ed2.title.contains(value)) {
return -1;
} else if (!ed1.title.contains(value) && ed2.title.contains(value)) {
return 1;
} else {
return 0;
}
}

/**
* Compares two ErrorDiscussion objects based on the presence of a third-level match with the error code in their titles and returns the result.
*
* @param ed1 The first ErrorDiscussion object.
* @param ed2 The second ErrorDiscussion object.
* @return An integer indicating the comparison result based on the presence of a third-level match with the error code in the titles:
* - A negative value (-1) if ed1 has a third-level match with the error code in the title and ed2 does not have a match,
* - A positive value (1) if ed1 does not have a match and ed2 has a third-level match with the error code in the title,
* - Or 0 if both ErrorDiscussion objects either have a third-level match or do not have a match with the error code in the titles.
*/
public int compareThirdLevelMatch(ErrorDiscussion ed1, ErrorDiscussion ed2) {
String value = " " + errorCode.methodCode();
if (ed1.title.contains(value) && !ed2.title.contains(value)) {
return -1;
} else if (!ed1.title.contains(value) && ed2.title.contains(value)) {
return 1;
} else {
return 0;
}
}

/* Getter/Setter */
public boolean isPreviousScenePresent() {
return previousScene != null;
}
Expand All @@ -125,4 +276,21 @@ public BooleanProperty copiedDetailsProperty() {
public boolean getCopiedDetails() {
return copiedDetails.get();
}
}

public BooleanProperty errorSolutionFoundProperty() {
return errorSolutionFound;
}

public boolean getErrorSolutionFound() {
return errorSolutionFound.get();
}

public BooleanProperty isLoadingHttpResponseProperty() {
return isLoadingHttpResponse;
}

public boolean getIsLoadingHttpResponse() {
return isLoadingHttpResponse.get();
}

}
12 changes: 12 additions & 0 deletions src/main/java/org/cryptomator/ui/error/ErrorDiscussion.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.cryptomator.ui.error;

public class ErrorDiscussion {

int upvoteCount;
String title;
String url;
Answer answer;

static class Answer{
}
}
33 changes: 21 additions & 12 deletions src/main/resources/fxml/error.fxml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>

<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import org.cryptomator.ui.controls.FontAwesome5Spinner?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
Expand Down Expand Up @@ -31,20 +32,28 @@
</StackPane>
<VBox spacing="6" HBox.hgrow="ALWAYS">
<FormattedLabel styleClass="label-extra-large" format="%error.message" arg1="${controller.errorCode}"/>
<Label text="%error.description" wrapText="true"/>
<Hyperlink styleClass="hyperlink-underline" text="%error.hyperlink.lookup" onAction="#searchError" contentDisplay="LEFT">
<graphic>
<FontAwesome5IconView glyph="LINK" glyphSize="12"/>
</graphic>
</Hyperlink>
<Hyperlink styleClass="hyperlink-underline" text="%error.hyperlink.report" onAction="#reportError" contentDisplay="LEFT">
<graphic>
<FontAwesome5IconView glyph="LINK" glyphSize="12"/>
</graphic>
</Hyperlink>
<FontAwesome5Spinner glyphSize="24" visible="${controller.isLoadingHttpResponse}" managed="${controller.isLoadingHttpResponse}"/>
<VBox visible="${!controller.isLoadingHttpResponse}" managed="${!controller.isLoadingHttpResponse}">
<Label text="%error.existingSolutionDescription" wrapText="true" visible="${controller.errorSolutionFound}" managed="${controller.errorSolutionFound}"/>
<Hyperlink styleClass="hyperlink-underline" text="%error.hyperlink.solution" onAction="#showSolution" contentDisplay="LEFT" visible="${controller.errorSolutionFound}" managed="${controller.errorSolutionFound}">
<graphic>
<FontAwesome5IconView glyph="LINK" glyphSize="12"/>
</graphic>
</Hyperlink>
<Label text="%error.description" wrapText="true" visible="${!controller.errorSolutionFound}" managed="${!controller.errorSolutionFound}"/>
<Hyperlink styleClass="hyperlink-underline" text="%error.hyperlink.lookup" onAction="#searchError" contentDisplay="LEFT" visible="${!controller.errorSolutionFound}" managed="${!controller.errorSolutionFound}">
<graphic>
<FontAwesome5IconView glyph="LINK" glyphSize="12"/>
</graphic>
</Hyperlink>
<Hyperlink styleClass="hyperlink-underline" text="%error.hyperlink.report" onAction="#reportError" contentDisplay="LEFT" visible="${!controller.errorSolutionFound}" managed="${!controller.errorSolutionFound}">
<graphic>
<FontAwesome5IconView glyph="LINK" glyphSize="12"/>
</graphic>
</Hyperlink>
</VBox>
</VBox>
</HBox>

<VBox spacing="6" VBox.vgrow="ALWAYS">
<HBox>
<Label text="%error.technicalDetails"/>
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/i18n/strings.properties
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ error.description=Cryptomator didn't expect this to happen. You can look up exis
error.hyperlink.lookup=Look up this error
error.hyperlink.report=Report this error
error.technicalDetails=Details:
error.existingSolutionDescription=Cryptomator didn't expect this to happen. But we found an existing solution for this error. Please take a look at the following link.
error.hyperlink.solution=Look up the solution


# Defaults
defaults.vault.vaultName=Vault
Expand Down