소스 검색

Merge pull request #2553 from cryptomator/feature/health-check-fix-batch

Improve Health Check (Fix-all-button, result filtering and more)
Armin Schrenk 2 년 전
부모
커밋
7f79c554f7

+ 1 - 0
src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java

@@ -27,6 +27,7 @@ public enum FontAwesome5Icon {
 	FILE("\uF15B"), //
 	FILE_IMPORT("\uF56F"), //
 	FOLDER_OPEN("\uF07C"), //
+	FUNNEL("\uF0B0"), //
 	HAND_HOLDING_HEART("\uF4BE"), //
 	HEART("\uF004"), //
 	HDD("\uF0A0"), //

+ 155 - 6
src/main/java/org/cryptomator/ui/health/CheckDetailController.java

@@ -8,15 +8,34 @@ import org.cryptomator.ui.common.FxController;
 
 import javax.inject.Inject;
 import javafx.beans.binding.Binding;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanBinding;
 import javafx.beans.binding.BooleanExpression;
+import javafx.beans.property.BooleanProperty;
 import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.beans.property.SimpleObjectProperty;
 import javafx.beans.value.ObservableValue;
 import javafx.collections.FXCollections;
 import javafx.fxml.FXML;
+import javafx.scene.control.ChoiceBox;
 import javafx.scene.control.ListView;
+import javafx.scene.input.Clipboard;
+import javafx.scene.input.ClipboardContent;
+import javafx.util.StringConverter;
+import java.util.Arrays;
+import java.util.ResourceBundle;
 import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 
+import static org.cryptomator.cryptofs.health.api.DiagnosticResult.Severity;
+import static org.cryptomator.ui.health.Result.FixState.FIXABLE;
+import static org.cryptomator.ui.health.Result.FixState.FIXED;
+import static org.cryptomator.ui.health.Result.FixState.FIXING;
+import static org.cryptomator.ui.health.Result.FixState.FIX_FAILED;
+import static org.cryptomator.ui.health.Result.FixState.NOT_FIXABLE;
+
 @HealthCheckScoped
 public class CheckDetailController implements FxController {
 
@@ -35,30 +54,48 @@ public class CheckDetailController implements FxController {
 	private final Binding<Number> countOfCritSeverity;
 	private final Binding<Boolean> warnOrCritsExist;
 	private final ResultListCellFactory resultListCellFactory;
+	private final ResultFixApplier resultFixApplier;
+	private final ResourceBundle resourceBundle;
+
+	private final BooleanProperty fixAllInfoResultsExecuted;
+	private final BooleanBinding fixAllInfoResultsPossible;
+	private final ObjectProperty<Predicate<Result>> resultsFilter;
 
 	public ListView<Result> resultsListView;
+	public ChoiceBox<DiagnosticResult.Severity> severityChoiceBox;
+	public ChoiceBox<Result.FixState> fixStateChoiceBox;
 	private Subscription resultSubscription;
 
 	@Inject
-	public CheckDetailController(ObjectProperty<Check> selectedTask, ResultListCellFactory resultListCellFactory) {
+	public CheckDetailController(ObjectProperty<Check> selectedTask, ResultListCellFactory resultListCellFactory, ResultFixApplier resultFixApplier, ResourceBundle resourceBundle) {
 		this.resultListCellFactory = resultListCellFactory;
+		this.resultFixApplier = resultFixApplier;
+		this.resourceBundle = resourceBundle;
 		this.results = EasyBind.wrapList(FXCollections.observableArrayList());
 		this.check = selectedTask;
 		this.checkState = selectedTask.flatMap(Check::stateProperty);
 		this.checkName = selectedTask.map(Check::getName).orElse("");
 		this.checkRunning = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.RUNNING::equals).orElse(false));
 		this.checkScheduled = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.SCHEDULED::equals).orElse(false));
-		this.checkSkipped =BooleanExpression.booleanExpression(checkState.map(Check.CheckState.SKIPPED::equals).orElse(false));
+		this.checkSkipped = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.SKIPPED::equals).orElse(false));
 		this.checkSucceeded = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.SUCCEEDED::equals).orElse(false));
 		this.checkFailed = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.ERROR::equals).orElse(false));
 		this.checkCancelled = BooleanExpression.booleanExpression(checkState.map(Check.CheckState.CANCELLED::equals).orElse(false));
 		this.checkFinished = checkSucceeded.or(checkFailed).or(checkCancelled);
-		this.countOfWarnSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.WARN));
-		this.countOfCritSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.CRITICAL));
-		this.warnOrCritsExist = EasyBind.combine(checkSucceeded, countOfWarnSeverity, countOfCritSeverity, (suceeded, warns, crits) -> suceeded && (warns.longValue() > 0 || crits.longValue() > 0) );
+		this.countOfWarnSeverity = results.reduce(countSeverity(Severity.WARN));
+		this.countOfCritSeverity = results.reduce(countSeverity(Severity.CRITICAL));
+		this.warnOrCritsExist = EasyBind.combine(checkSucceeded, countOfWarnSeverity, countOfCritSeverity, (suceeded, warns, crits) -> suceeded && (warns.longValue() > 0 || crits.longValue() > 0));
+		this.fixAllInfoResultsExecuted = new SimpleBooleanProperty(false);
+		this.fixAllInfoResultsPossible = Bindings.createBooleanBinding(() -> results.stream().anyMatch(this::isFixableInfoResult), results) //
+				.and(fixAllInfoResultsExecuted.not());
+		this.resultsFilter = new SimpleObjectProperty<>(r -> true);
 		selectedTask.addListener(this::selectedTaskChanged);
 	}
 
+	private boolean isFixableInfoResult(Result r) {
+		return r.diagnosis().getSeverity() == Severity.INFO && r.getState() == FIXABLE;
+	}
+
 	private void selectedTaskChanged(ObservableValue<? extends Check> observable, Check oldValue, Check newValue) {
 		if (resultSubscription != null) {
 			resultSubscription.unsubscribe();
@@ -66,6 +103,8 @@ public class CheckDetailController implements FxController {
 		if (newValue != null) {
 			resultSubscription = EasyBind.bindContent(results, newValue.getResults());
 		}
+		severityChoiceBox.setValue(null);
+		fixStateChoiceBox.setValue(null);
 	}
 
 	private Function<Stream<? extends Result>, Long> countSeverity(DiagnosticResult.Severity severity) {
@@ -74,8 +113,110 @@ public class CheckDetailController implements FxController {
 
 	@FXML
 	public void initialize() {
-		resultsListView.setItems(results);
+		resultsListView.setItems(results.filtered(resultsFilter));
 		resultsListView.setCellFactory(resultListCellFactory);
+
+		severityChoiceBox.getItems().add(null);
+		severityChoiceBox.getItems().addAll(Arrays.stream(DiagnosticResult.Severity.values()).toList());
+		severityChoiceBox.setConverter(new SeverityStringifier());
+		severityChoiceBox.setValue(null);
+
+		fixStateChoiceBox.getItems().add(null);
+		fixStateChoiceBox.getItems().addAll(Arrays.stream(Result.FixState.values()).toList());
+		fixStateChoiceBox.setConverter(new FixStateStringifier());
+		fixStateChoiceBox.setValue(null);
+
+		resultsFilter.bind(Bindings.createObjectBinding(() -> this::filterResults, severityChoiceBox.valueProperty(), fixStateChoiceBox.valueProperty()));
+	}
+
+	private boolean filterResults(Result r) {
+		var desiredFixState = fixStateChoiceBox.getValue();
+		var desiredSeverity = severityChoiceBox.getValue();
+		return (desiredFixState == null || r.getState() == desiredFixState) && (desiredSeverity == null || r.diagnosis().getSeverity() == desiredSeverity);
+	}
+
+	@FXML
+	public void fixAllInfoResults() {
+		fixAllInfoResultsExecuted.setValue(true);
+		results.stream().filter(this::isFixableInfoResult).forEach(resultFixApplier::fix);
+	}
+
+
+	@FXML
+	public void copyResultDetails() {
+		var result = resultsListView.getSelectionModel().getSelectedItem();
+		if (result != null) {
+			ClipboardContent clipboardContent = new ClipboardContent();
+			clipboardContent.putString(result.diagnosis().toString());
+			Clipboard.getSystemClipboard().setContent(clipboardContent);
+		}
+	}
+
+	/* -- Internal classes -- */
+
+	class SeverityStringifier extends StringConverter<Severity> {
+
+		@Override
+		public String toString(Severity object) {
+			if (object == null) {
+				return resourceBundle.getString("health.result.severityFilter.all");
+			}
+			return switch (object) {
+				case GOOD -> resourceBundle.getString("health.result.severityFilter.good");
+				case INFO -> resourceBundle.getString("health.result.severityFilter.info");
+				case WARN -> resourceBundle.getString("health.result.severityFilter.warn");
+				case CRITICAL -> resourceBundle.getString("health.result.severityFilter.crit");
+			};
+		}
+
+		@Override
+		public Severity fromString(String string) {
+			if (resourceBundle.getString("health.result.severityFilter.good").equals(string)) {
+				return Severity.GOOD;
+			} else if (resourceBundle.getString("health.result.severityFilter.info").equals(string)) {
+				return Severity.INFO;
+			} else if (resourceBundle.getString("health.result.severityFilter.warn").equals(string)) {
+				return Severity.WARN;
+			} else if (resourceBundle.getString("health.result.severityFilter.crit").equals(string)) {
+				return Severity.CRITICAL;
+			} else {
+				return null;
+			}
+		}
+	}
+
+	class FixStateStringifier extends StringConverter<Result.FixState> {
+
+		@Override
+		public String toString(Result.FixState object) {
+			if (object == null) {
+				return resourceBundle.getString("health.result.fixStateFilter.all");
+			}
+			return switch (object) {
+				case FIXABLE -> resourceBundle.getString("health.result.fixStateFilter.fixable");
+				case NOT_FIXABLE -> resourceBundle.getString("health.result.fixStateFilter.notFixable");
+				case FIXING -> resourceBundle.getString("health.result.fixStateFilter.fixing");
+				case FIXED -> resourceBundle.getString("health.result.fixStateFilter.fixed");
+				case FIX_FAILED -> resourceBundle.getString("health.result.fixStateFilter.fixFailed");
+			};
+		}
+
+		@Override
+		public Result.FixState fromString(String string) {
+			if (resourceBundle.getString("health.result.fixStateFilter.fixable").equals(string)) {
+				return FIXABLE;
+			} else if (resourceBundle.getString("health.result.fixStateFilter.notFixable").equals(string)) {
+				return NOT_FIXABLE;
+			} else if (resourceBundle.getString("health.result.fixStateFilter.fixing").equals(string)) {
+				return FIXING;
+			} else if (resourceBundle.getString("health.result.fixStateFilter.fixed").equals(string)) {
+				return FIXED;
+			} else if (resourceBundle.getString("health.result.fixStateFilter.fixFailed").equals(string)) {
+				return FIX_FAILED;
+			} else {
+				return null;
+			}
+		}
 	}
 
 	/* Getter/Setter */
@@ -175,4 +316,12 @@ public class CheckDetailController implements FxController {
 	public Check getCheck() {
 		return check.get();
 	}
+
+	public ObservableValue<Boolean> fixAllInfoResultsPossibleProperty() {
+		return fixAllInfoResultsPossible;
+	}
+
+	public boolean getFixAllInfoResultsPossible() {
+		return fixAllInfoResultsPossible.getValue();
+	}
 }

+ 1 - 1
src/main/java/org/cryptomator/ui/health/CheckExecutor.java

@@ -70,7 +70,7 @@ public class CheckExecutor {
 			try (var masterkeyClone = masterkey.copy(); //
 				 var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) {
 				c.getHealthCheck().check(vaultPath, vaultConfig, masterkeyClone, cryptor, diagnosis -> {
-					Platform.runLater(() -> c.getResults().add(Result.create(diagnosis)));
+					Platform.runLater(() -> c.getResults().add(Result.create(diagnosis, vaultPath, vaultConfig, masterkeyClone, cryptor)));
 					highestResultSeverity = Comparators.max(highestResultSeverity, diagnosis.getSeverity());
 				});
 			}

+ 1 - 1
src/main/java/org/cryptomator/ui/health/CheckStateIconView.java

@@ -31,7 +31,7 @@ public class CheckStateIconView extends FontAwesome5IconView {
 		this.severity = EasyBind.wrapNullable(check).mapObservable(Check::highestResultSeverityProperty).asOrdinary();
 		this.glyph.bind(Bindings.createObjectBinding(this::glyphForState, state, severity));
 		this.subscriptions = List.of( //
-				EasyBind.includeWhen(getStyleClass(), "glyph-icon-muted", Bindings.equal(state, Check.CheckState.SKIPPED).or(Bindings.equal(state, Check.CheckState.CANCELLED))), //
+				EasyBind.includeWhen(getStyleClass(), "glyph-icon-muted", Bindings.equal(state, Check.CheckState.SKIPPED).or(Bindings.equal(state, Check.CheckState.CANCELLED)).or(Bindings.equal(severity, DiagnosticResult.Severity.INFO))), //
 				EasyBind.includeWhen(getStyleClass(), "glyph-icon-primary", Bindings.equal(severity, DiagnosticResult.Severity.GOOD)), //
 				EasyBind.includeWhen(getStyleClass(), "glyph-icon-orange", Bindings.equal(severity, DiagnosticResult.Severity.WARN).or(Bindings.equal(severity, DiagnosticResult.Severity.CRITICAL))), //
 				EasyBind.includeWhen(getStyleClass(), "glyph-icon-red", Bindings.equal(state, Check.CheckState.ERROR)) //

+ 6 - 2
src/main/java/org/cryptomator/ui/health/Result.java

@@ -1,10 +1,14 @@
 package org.cryptomator.ui.health;
 
+import org.cryptomator.cryptofs.VaultConfig;
 import org.cryptomator.cryptofs.health.api.DiagnosticResult;
+import org.cryptomator.cryptolib.api.Cryptor;
+import org.cryptomator.cryptolib.api.Masterkey;
 
 import javafx.beans.Observable;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
+import java.nio.file.Path;
 
 record Result(DiagnosticResult diagnosis, ObjectProperty<FixState> fixState) {
 
@@ -16,8 +20,8 @@ record Result(DiagnosticResult diagnosis, ObjectProperty<FixState> fixState) {
 		FIX_FAILED
 	}
 
-	public static Result create(DiagnosticResult diagnosis) {
-		FixState initialState = diagnosis.getSeverity() == DiagnosticResult.Severity.WARN ? FixState.FIXABLE : FixState.NOT_FIXABLE;
+	public static Result create(DiagnosticResult diagnosis, Path vaultPath, VaultConfig config, Masterkey masterkey, Cryptor cryptor) {
+		FixState initialState = diagnosis.getFix(vaultPath, config, masterkey, cryptor).map( _f -> FixState.FIXABLE).orElse(FixState.NOT_FIXABLE);
 		return new Result(diagnosis, new SimpleObjectProperty<>(initialState));
 	}
 

+ 18 - 7
src/main/java/org/cryptomator/ui/health/ResultFixApplier.java

@@ -23,6 +23,8 @@ import java.util.concurrent.atomic.AtomicReference;
 @HealthCheckScoped
 class ResultFixApplier {
 
+	private static final Logger LOG = LoggerFactory.getLogger(ResultFixApplier.class);
+
 	private final Path vaultPath;
 	private final SecureRandom csprng;
 	private final Masterkey masterkey;
@@ -40,25 +42,34 @@ class ResultFixApplier {
 
 	public CompletionStage<Void> fix(Result result) {
 		Preconditions.checkArgument(result.getState() == Result.FixState.FIXABLE);
-		result.setState(Result.FixState.FIXING);
-		return CompletableFuture.runAsync(() -> fix(result.diagnosis()), sequentialExecutor)
+		return CompletableFuture.runAsync(() -> result.setState(Result.FixState.FIXING), Platform::runLater) //
+				.thenRunAsync(() -> fix(result.diagnosis()), sequentialExecutor) //
 				.whenCompleteAsync((unused, throwable) -> {
-					var fixed = throwable == null ? Result.FixState.FIXED : Result.FixState.FIX_FAILED;
-					result.setState(fixed);
+					final Result.FixState s;
+					if (throwable == null) {
+						LOG.debug("Fix for {} applied successful.", result.diagnosis().getClass().getName());
+						s = Result.FixState.FIXED;
+					} else {
+						LOG.error("Failed to apply fix for {}", result.diagnosis().getClass().getName(), throwable);
+						s = Result.FixState.FIX_FAILED;
+					}
+					result.setState(s);
 				}, Platform::runLater);
 	}
 
-	public void fix(DiagnosticResult diagnosis) {
-		Preconditions.checkArgument(diagnosis.getSeverity() == DiagnosticResult.Severity.WARN, "Unfixable result");
+	private void fix(DiagnosticResult diagnosis) {
 		try (var masterkeyClone = masterkey.copy(); //
 			 var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) {
-			diagnosis.fix(vaultPath, vaultConfig, masterkeyClone, cryptor);
+			diagnosis.getFix(vaultPath, vaultConfig, masterkeyClone, cryptor) //
+					.orElseThrow(() -> new IllegalStateException("No fix for diagnosis " + diagnosis.getClass().getName() + " implemented.")) //
+					.apply();
 		} catch (Exception e) {
 			throw new FixFailedException(e);
 		}
 	}
 
 	public static class FixFailedException extends CompletionException {
+
 		private FixFailedException(Throwable cause) {
 			super(cause);
 		}

+ 48 - 34
src/main/java/org/cryptomator/ui/health/ResultListCellController.java

@@ -12,7 +12,6 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
-import javafx.application.Platform;
 import javafx.beans.binding.Bindings;
 import javafx.beans.binding.BooleanBinding;
 import javafx.beans.binding.ObjectBinding;
@@ -34,7 +33,6 @@ public class ResultListCellController implements FxController {
 	private static final FontAwesome5Icon WARN_ICON = FontAwesome5Icon.EXCLAMATION_TRIANGLE;
 	private static final FontAwesome5Icon CRIT_ICON = FontAwesome5Icon.TIMES;
 
-	private final Logger LOG = LoggerFactory.getLogger(ResultListCellController.class);
 
 	private final ObjectProperty<Result> result;
 	private final ObservableValue<DiagnosticResult.Severity> severity;
@@ -42,17 +40,17 @@ public class ResultListCellController implements FxController {
 	private final ResultFixApplier fixApplier;
 	private final ObservableValue<Result.FixState> fixState;
 	private final ObjectBinding<FontAwesome5Icon> severityGlyph;
-	private final ObjectBinding<FontAwesome5Icon> fixGlyph;
 	private final BooleanBinding fixable;
 	private final BooleanBinding fixing;
 	private final BooleanBinding fixed;
 	private final BooleanBinding fixFailed;
 	private final BooleanBinding fixRunningOrDone;
+	private final ObservableValue<FontAwesome5Icon> fixGlyph;
 	private final List<Subscription> subscriptions;
-	private final Tooltip fixSuccess;
-	private final Tooltip fixFail;
-
+	private final Tooltip fixStateTip;
+	private final Tooltip severityTip;
 	private AutoAnimator fixRunningRotator;
+	private final ResourceBundle resourceBundle;
 
 	/* FXML */
 	public FontAwesome5IconView severityView;
@@ -60,23 +58,54 @@ public class ResultListCellController implements FxController {
 
 	@Inject
 	public ResultListCellController(ResultFixApplier fixApplier, ResourceBundle resourceBundle) {
+		this.resourceBundle = resourceBundle;
 		this.result = new SimpleObjectProperty<>(null);
 		this.severity = result.map(Result::diagnosis).map(DiagnosticResult::getSeverity);
 		this.description = result.map(Result::getDescription).orElse("");
 		this.fixApplier = fixApplier;
 		this.fixState = result.flatMap(Result::fixState);
 		this.severityGlyph = Bindings.createObjectBinding(this::getSeverityGlyph, result);
-		this.fixGlyph = Bindings.createObjectBinding(this::getFixGlyph, fixState);
 		this.fixable = Bindings.createBooleanBinding(this::isFixable, fixState);
 		this.fixing = Bindings.createBooleanBinding(this::isFixing, fixState);
 		this.fixed = Bindings.createBooleanBinding(this::isFixed, fixState);
 		this.fixFailed = Bindings.createBooleanBinding(this::isFixFailed, fixState);
 		this.fixRunningOrDone = fixing.or(fixed).or(fixFailed);
+		this.fixGlyph = fixState.map(this::getFixGlyph);
 		this.subscriptions = new ArrayList<>();
-		this.fixSuccess = new Tooltip(resourceBundle.getString("health.fix.successTip"));
-		this.fixFail = new Tooltip(resourceBundle.getString("health.fix.failTip"));
-		fixSuccess.setShowDelay(Duration.millis(100));
-		fixFail.setShowDelay(Duration.millis(100));
+
+		this.fixStateTip = new Tooltip();
+		fixStateTip.textProperty().bind(fixState.map(this::getFixStateDescription));
+		fixStateTip.setShowDelay(Duration.millis(100));
+
+		this.severityTip = new Tooltip();
+		severityTip.textProperty().bind(severity.map(this::getSeverityDescription));
+		severityTip.setShowDelay(Duration.millis(150));
+	}
+
+	public FontAwesome5Icon getFixGlyph(Result.FixState state) {
+		return switch (state) {
+			case FIXING -> FontAwesome5Icon.SPINNER;
+			case FIXED -> FontAwesome5Icon.CHECK;
+			case FIX_FAILED -> FontAwesome5Icon.TIMES;
+			default -> null;
+		};
+	}
+
+	private String getFixStateDescription(Result.FixState fixState) {
+		return switch (fixState) {
+			case FIXED -> resourceBundle.getString("health.fix.successTip");
+			case FIX_FAILED -> resourceBundle.getString("health.fix.failTip");
+			default -> "";
+		};
+	}
+
+	private String getSeverityDescription(DiagnosticResult.Severity severity) {
+		return resourceBundle.getString(switch (severity) {
+			case GOOD -> "health.result.severityTip.good";
+			case INFO -> "health.result.severityTip.info";
+			case WARN -> "health.result.severityTip.warn";
+			case CRITICAL -> "health.result.severityTip.crit";
+		});
 	}
 
 	@FXML
@@ -93,22 +122,19 @@ public class ResultListCellController implements FxController {
 				.onCondition(fixing) //
 				.afterStop(() -> fixView.setRotate(0)) //
 				.build();
+		fixState.addListener(((observable, oldValue, newValue) -> {
+			if (newValue == Result.FixState.FIXED || newValue == Result.FixState.FIX_FAILED) {
+				Tooltip.install(fixView, fixStateTip);
+			}
+		}));
+		Tooltip.install(severityView, severityTip);
 	}
 
 	@FXML
 	public void fix() {
 		Result r = result.get();
 		if (r != null) {
-			fixApplier.fix(r).whenCompleteAsync(this::fixFinished, Platform::runLater);
-		}
-	}
-
-	private void fixFinished(Void unused, Throwable exception) {
-		if (exception != null) {
-			LOG.error("Failed to apply fix", exception);
-			Tooltip.install(fixView, fixFail);
-		} else {
-			Tooltip.install(fixView, fixSuccess);
+			fixApplier.fix(r);
 		}
 	}
 
@@ -152,22 +178,10 @@ public class ResultListCellController implements FxController {
 		};
 	}
 
-	public ObjectBinding<FontAwesome5Icon> fixGlyphProperty() {
+	public ObservableValue<FontAwesome5Icon> fixGlyphProperty() {
 		return fixGlyph;
 	}
 
-	public FontAwesome5Icon getFixGlyph() {
-		if (fixState.getValue() == null) {
-			return null;
-		}
-		return switch (fixState.getValue()) {
-			case NOT_FIXABLE, FIXABLE -> null;
-			case FIXING -> FontAwesome5Icon.SPINNER;
-			case FIXED -> FontAwesome5Icon.CHECK;
-			case FIX_FAILED -> FontAwesome5Icon.TIMES;
-		};
-	}
-
 	public BooleanBinding fixableProperty() {
 		return fixable;
 	}

+ 51 - 17
src/main/resources/fxml/health_check_details.fxml

@@ -5,25 +5,59 @@
 <?import javafx.scene.layout.VBox?>
 <?import org.cryptomator.ui.health.CheckStateIconView?>
 <?import javafx.scene.layout.HBox?>
+<?import javafx.scene.control.Button?>
+<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
+<?import javafx.scene.layout.Region?>
+<?import javafx.scene.control.ChoiceBox?>
+<?import javafx.scene.control.ContextMenu?>
+<?import javafx.scene.control.MenuItem?>
 <VBox xmlns:fx="http://javafx.com/fxml"
 	  xmlns="http://javafx.com/javafx"
 	  fx:controller="org.cryptomator.ui.health.CheckDetailController"
-	  spacing="6">
-	<Label fx:id="detailHeader" styleClass="label-extra-large" text="${controller.checkName}" contentDisplay="LEFT">
-		<graphic>
-			<HBox alignment="CENTER" minWidth="25" maxWidth="25">
-				<CheckStateIconView fx:id="checkStateIconView" check="${controller.check}" glyphSize="20"/>
-			</HBox>
-		</graphic>
-	</Label>
+	  spacing="12">
+	<HBox alignment="CENTER" >
+		<VBox spacing="12">
+			<Label fx:id="detailHeader" styleClass="label-extra-large" text="${controller.checkName}" contentDisplay="RIGHT">
+				<graphic>
+					<HBox alignment="CENTER" minWidth="25" maxWidth="25">
+						<CheckStateIconView check="${controller.check}" glyphSize="20"/>
+					</HBox>
+				</graphic>
+			</Label>
+			<Label text="%health.check.detail.checkRunning" visible="${controller.checkRunning}" managed="${controller.checkRunning}"/>
+			<Label text="%health.check.detail.checkScheduled" visible="${controller.checkScheduled}" managed="${controller.checkScheduled}"/>
+			<Label text="%health.check.detail.checkSkipped" visible="${controller.checkSkipped}" managed="${controller.checkSkipped}"/>
+			<Label text="%health.check.detail.checkCancelled" visible="${controller.checkCancelled}" managed="${controller.checkCancelled}"/>
+			<Label text="%health.check.detail.checkFailed" visible="${controller.checkFailed}" managed="${controller.checkFailed}"/>
+			<Label text="%health.check.detail.checkFinished" visible="${controller.checkSucceeded &amp;&amp; !controller.warnOrCritsExist}" managed="${controller.checkSucceeded &amp;&amp; !controller.warnOrCritsExist}"/>
+			<Label text="%health.check.detail.checkFinishedAndFound" visible="${controller.checkSucceeded &amp;&amp; controller.warnOrCritsExist}" managed="${controller.checkSucceeded &amp;&amp; controller.warnOrCritsExist}"/>
+		</VBox>
+		<Region HBox.hgrow="ALWAYS"/>
+		<Button text="%health.check.detail.fixAllSpecificBtn" contentDisplay="RIGHT" graphicTextGap="3" visible="${controller.checkFinished}" managed="${controller.checkFinished}" disable="${! controller.fixAllInfoResultsPossible}" onAction="#fixAllInfoResults">
+			<graphic>
+				<FontAwesome5IconView glyph="INFO_CIRCLE" glyphSize="12" styleClass="glyph-icon-muted"/>
+			</graphic>
+		</Button>
 
-	<Label text="%health.check.detail.checkRunning" visible="${controller.checkRunning}" managed="${controller.checkRunning}"/>
-	<Label text="%health.check.detail.checkScheduled" visible="${controller.checkScheduled}" managed="${controller.checkScheduled}"/>
-	<Label text="%health.check.detail.checkSkipped" visible="${controller.checkSkipped}" managed="${controller.checkSkipped}"/>
-	<Label text="%health.check.detail.checkCancelled" visible="${controller.checkCancelled}" managed="${controller.checkCancelled}"/>
-	<Label text="%health.check.detail.checkFailed" visible="${controller.checkFailed}" managed="${controller.checkFailed}"/>
-	<Label text="%health.check.detail.checkFinished" visible="${controller.checkSucceeded &amp;&amp; !controller.warnOrCritsExist}" managed="${controller.checkSucceeded &amp;&amp; !controller.warnOrCritsExist}"/>
-	<Label text="%health.check.detail.checkFinishedAndFound" visible="${controller.checkSucceeded &amp;&amp; controller.warnOrCritsExist}" managed="${controller.checkSucceeded &amp;&amp; controller.warnOrCritsExist}"/>
-
-	<ListView fx:id="resultsListView" VBox.vgrow="ALWAYS" visible="${!controller.checkSkipped}" fixedCellSize="25"/>
+	</HBox>
+	<VBox spacing="6" VBox.vgrow="ALWAYS">
+		<HBox alignment="CENTER_LEFT" spacing="6">
+			<Label fx:id="filterLbl" text="%health.check.detail.listFilters.label">
+				<graphic>
+					<FontAwesome5IconView glyph="FUNNEL" glyphSize="${filterLbl.height}" styleClass="glyph-icon-muted"/>
+				</graphic>
+			</Label>
+			<ChoiceBox fx:id="severityChoiceBox" />
+			<ChoiceBox fx:id="fixStateChoiceBox" />
+		</HBox>
+		<ListView fx:id="resultsListView" VBox.vgrow="ALWAYS" visible="${!controller.checkSkipped}" fixedCellSize="25">
+			<contextMenu>
+				<ContextMenu>
+					<items>
+						<MenuItem text="%generic.button.copy" onAction="#copyResultDetails"/>
+					</items>
+				</ContextMenu>
+			</contextMenu>
+		</ListView>
+	</VBox>
 </VBox>

+ 1 - 1
src/main/resources/fxml/health_check_list.fxml

@@ -12,7 +12,7 @@
 <VBox xmlns:fx="http://javafx.com/fxml"
 	  xmlns="http://javafx.com/javafx"
 	  fx:controller="org.cryptomator.ui.health.CheckListController"
-	  prefWidth="600"
+	  prefWidth="650"
 	  prefHeight="400"
 	  spacing="12">
 	<padding>

+ 1 - 1
src/main/resources/fxml/health_start.fxml

@@ -16,7 +16,7 @@
 <VBox xmlns:fx="http://javafx.com/fxml"
 	  xmlns="http://javafx.com/javafx"
 	  fx:controller="org.cryptomator.ui.health.StartController"
-	  prefWidth="600"
+	  prefWidth="650"
 	  prefHeight="400"
 	  spacing="12">
 	<padding>

+ 20 - 0
src/main/resources/i18n/strings.properties

@@ -219,7 +219,27 @@ health.check.detail.checkFinished=The check finished successfully.
 health.check.detail.checkFinishedAndFound=The check finished running. Please review the results.
 health.check.detail.checkFailed=The check exited due to an error.
 health.check.detail.checkCancelled=The check was cancelled.
+health.check.detail.listFilters.label=Filter
+health.check.detail.listFilters.severity=Severity
+health.check.detail.listFilters.fixState=Fix state
+health.check.detail.fixAllSpecificBtn=Fix all of type
 health.check.exportBtn=Export Report
+## Result view
+health.result.severityFilter.all=Severity - All
+health.result.severityFilter.good=Good
+health.result.severityFilter.info=Info
+health.result.severityFilter.warn=Warning
+health.result.severityFilter.crit=Critical
+health.result.severityTip.good=Severity: Good\nNormal vault structure.
+health.result.severityTip.info=Severity: Info\nVault structure intact, fix suggested.
+health.result.severityTip.warn=Severity: Warning\nVault structure corrupted, fix highly advised.
+health.result.severityTip.crit=Severity: Critical\nVault structure corrupted, data loss determined.
+health.result.fixStateFilter.all=Fix state - All
+health.result.fixStateFilter.fixable=Fixable
+health.result.fixStateFilter.notFixable=Not fixable
+health.result.fixStateFilter.fixing=Fixing…
+health.result.fixStateFilter.fixed=Fixed
+health.result.fixStateFilter.fixFailed=Fix failed
 ## Fix Application
 health.fix.fixBtn=Fix
 health.fix.successTip=Fix successful