mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-04-19 17:16:53 -04:00
Merge pull request #1712 from cryptomator/feature/improve-health-check
Refactor health check to improve UX
This commit is contained in:
@@ -44,6 +44,7 @@ module org.cryptomator.desktop {
|
||||
opens org.cryptomator.ui.controls to javafx.fxml;
|
||||
opens org.cryptomator.ui.forgetPassword to javafx.fxml;
|
||||
opens org.cryptomator.ui.fxapp to javafx.fxml;
|
||||
opens org.cryptomator.ui.health to javafx.fxml;
|
||||
opens org.cryptomator.ui.keyloading.masterkeyfile to javafx.fxml;
|
||||
opens org.cryptomator.ui.mainwindow to javafx.fxml;
|
||||
opens org.cryptomator.ui.migration to javafx.fxml;
|
||||
|
||||
@@ -19,6 +19,7 @@ import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
|
||||
import org.cryptomator.cryptofs.CryptoFileSystemProvider;
|
||||
import org.cryptomator.cryptofs.VaultConfig;
|
||||
import org.cryptomator.cryptofs.VaultConfig.UnverifiedVaultConfig;
|
||||
import org.cryptomator.cryptofs.VaultConfigLoadException;
|
||||
import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
||||
@@ -327,6 +328,14 @@ public class Vault {
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to read the vault config file and parse it without verifying its integrity.
|
||||
*
|
||||
* @return an unverified vault config
|
||||
* @throws VaultConfigLoadException if the read file cannot be properly parsed
|
||||
* @throws IOException if reading the file fails
|
||||
*
|
||||
*/
|
||||
public UnverifiedVaultConfig getUnverifiedVaultConfig() throws IOException {
|
||||
Path configPath = getPath().resolve(org.cryptomator.common.Constants.VAULTCONFIG_FILENAME);
|
||||
String token = Files.readString(configPath, StandardCharsets.US_ASCII);
|
||||
|
||||
@@ -12,6 +12,7 @@ public enum FxmlFile {
|
||||
ERROR("/fxml/error.fxml"), //
|
||||
FORGET_PASSWORD("/fxml/forget_password.fxml"), //
|
||||
HEALTH_START("/fxml/health_start.fxml"), //
|
||||
HEALTH_START_FAIL("/fxml/health_start_fail.fxml"), //
|
||||
HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), //
|
||||
LOCK_FORCED("/fxml/lock_forced.fxml"), //
|
||||
LOCK_FAILED("/fxml/lock_failed.fxml"), //
|
||||
|
||||
@@ -8,6 +8,8 @@ public enum FontAwesome5Icon {
|
||||
ARROW_UP("\uF062"), //
|
||||
BAN("\uF05E"), //
|
||||
BUG("\uF188"), //
|
||||
CARET_DOWN("\uF0D7"), //
|
||||
CARET_RIGHT("\uF0Da"), //
|
||||
CHECK("\uF00C"), //
|
||||
CLOCK("\uF017"), //
|
||||
COG("\uF013"), //
|
||||
@@ -20,6 +22,7 @@ public enum FontAwesome5Icon {
|
||||
EXCLAMATION_TRIANGLE("\uF071"), //
|
||||
EYE("\uF06E"), //
|
||||
EYE_SLASH("\uF070"), //
|
||||
FAST_FORWARD("\uF050"), //
|
||||
FILE("\uF15B"), //
|
||||
FILE_IMPORT("\uF56F"), //
|
||||
FOLDER_OPEN("\uF07C"), //
|
||||
|
||||
@@ -21,8 +21,8 @@ public class FontAwesome5IconView extends Text {
|
||||
private static final String FONT_PATH = "/css/fontawesome5-free-solid.otf";
|
||||
private static final Font FONT;
|
||||
|
||||
private final ObjectProperty<FontAwesome5Icon> glyph = new SimpleObjectProperty<>(this, "glyph", DEFAULT_GLYPH);
|
||||
private final DoubleProperty glyphSize = new SimpleDoubleProperty(this, "glyphSize", DEFAULT_GLYPH_SIZE);
|
||||
protected final ObjectProperty<FontAwesome5Icon> glyph = new SimpleObjectProperty<>(this, "glyph", DEFAULT_GLYPH);
|
||||
protected final DoubleProperty glyphSize = new SimpleDoubleProperty(this, "glyphSize", DEFAULT_GLYPH_SIZE);
|
||||
|
||||
static {
|
||||
try {
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Suppliers;
|
||||
import dagger.Lazy;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class BatchService extends Service<Void> {
|
||||
|
||||
private final Iterator<HealthCheckTask> remainingTasks;
|
||||
|
||||
public BatchService(Iterable<HealthCheckTask> tasks) {
|
||||
this.remainingTasks = tasks.iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
Preconditions.checkState(remainingTasks.hasNext(), "No remaining tasks");
|
||||
return remainingTasks.next();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void succeeded() {
|
||||
if (remainingTasks.hasNext()) {
|
||||
this.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
103
src/main/java/org/cryptomator/ui/health/Check.java
Normal file
103
src/main/java/org/cryptomator/ui/health/Check.java
Normal file
@@ -0,0 +1,103 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||
import org.cryptomator.cryptofs.health.api.HealthCheck;
|
||||
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
public class Check {
|
||||
|
||||
private final HealthCheck check;
|
||||
|
||||
private final BooleanProperty chosenForExecution = new SimpleBooleanProperty(false);
|
||||
private final ObjectProperty<CheckState> state = new SimpleObjectProperty<>(CheckState.RUNNABLE);
|
||||
private final ObservableList<Result> results = FXCollections.observableArrayList(Result::observables);
|
||||
private final ObjectProperty<DiagnosticResult.Severity> highestResultSeverity = new SimpleObjectProperty<>(null);
|
||||
private final ObjectProperty<Throwable> error = new SimpleObjectProperty<>(null);
|
||||
private final BooleanBinding isInReRunState = state.isNotEqualTo(CheckState.RUNNING).or(state.isNotEqualTo(CheckState.SCHEDULED));
|
||||
|
||||
Check(HealthCheck check) {
|
||||
this.check = check;
|
||||
}
|
||||
|
||||
String getName() {
|
||||
return check.name();
|
||||
}
|
||||
|
||||
HealthCheck getHealthCheck() {
|
||||
return check;
|
||||
}
|
||||
|
||||
BooleanProperty chosenForExecutionProperty() {
|
||||
return chosenForExecution;
|
||||
}
|
||||
|
||||
boolean isChosenForExecution() {
|
||||
return chosenForExecution.get();
|
||||
}
|
||||
|
||||
ObjectProperty<CheckState> stateProperty() {
|
||||
return state;
|
||||
}
|
||||
|
||||
CheckState getState() {
|
||||
return state.get();
|
||||
}
|
||||
|
||||
void setState(CheckState newState) {
|
||||
state.set(newState);
|
||||
}
|
||||
|
||||
ObjectProperty<Throwable> errorProperty() {
|
||||
return error;
|
||||
}
|
||||
|
||||
Throwable getError() {
|
||||
return error.get();
|
||||
}
|
||||
|
||||
void setError(Throwable t) {
|
||||
error.set(t);
|
||||
}
|
||||
|
||||
ObjectProperty<DiagnosticResult.Severity> highestResultSeverityProperty() {
|
||||
return highestResultSeverity;
|
||||
}
|
||||
|
||||
DiagnosticResult.Severity getHighestResultSeverity() {
|
||||
return highestResultSeverity.get();
|
||||
}
|
||||
|
||||
void setHighestResultSeverity(DiagnosticResult.Severity severity) {
|
||||
highestResultSeverity.set(severity);
|
||||
}
|
||||
|
||||
boolean isInReRunState() {
|
||||
return isInReRunState.get();
|
||||
}
|
||||
|
||||
enum CheckState {
|
||||
RUNNABLE,
|
||||
SCHEDULED,
|
||||
RUNNING,
|
||||
SUCCEEDED,
|
||||
SKIPPED,
|
||||
ERROR,
|
||||
CANCELLED;
|
||||
}
|
||||
|
||||
ObservableList<Result> getResults() {
|
||||
return results;
|
||||
}
|
||||
|
||||
Observable[] observables() {
|
||||
return new Observable[]{chosenForExecution, state, results, error};
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,8 @@ import javafx.beans.binding.Binding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.ListView;
|
||||
import java.time.Duration;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -24,50 +21,50 @@ import java.util.stream.Stream;
|
||||
public class CheckDetailController implements FxController {
|
||||
|
||||
private final EasyObservableList<Result> results;
|
||||
private final OptionalBinding<Worker.State> taskState;
|
||||
private final Binding<String> taskName;
|
||||
private final Binding<String> taskDuration;
|
||||
private final Binding<Boolean> taskRunning;
|
||||
private final Binding<Boolean> taskScheduled;
|
||||
private final Binding<Boolean> taskFinished;
|
||||
private final Binding<Boolean> taskNotStarted;
|
||||
private final Binding<Boolean> taskSucceeded;
|
||||
private final Binding<Boolean> taskFailed;
|
||||
private final Binding<Boolean> taskCancelled;
|
||||
private final ObjectProperty<Check> check;
|
||||
private final OptionalBinding<Check.CheckState> checkState;
|
||||
private final Binding<String> checkName;
|
||||
private final Binding<Boolean> checkRunning;
|
||||
private final Binding<Boolean> checkScheduled;
|
||||
private final Binding<Boolean> checkFinished;
|
||||
private final Binding<Boolean> checkSkipped;
|
||||
private final Binding<Boolean> checkSucceeded;
|
||||
private final Binding<Boolean> checkFailed;
|
||||
private final Binding<Boolean> checkCancelled;
|
||||
private final Binding<Number> countOfWarnSeverity;
|
||||
private final Binding<Number> countOfCritSeverity;
|
||||
private final Binding<Boolean> warnOrCritsExist;
|
||||
private final ResultListCellFactory resultListCellFactory;
|
||||
private final ResourceBundle resourceBundle;
|
||||
|
||||
public ListView<Result> resultsListView;
|
||||
private Subscription resultSubscription;
|
||||
|
||||
@Inject
|
||||
public CheckDetailController(ObjectProperty<HealthCheckTask> selectedTask, ResultListCellFactory resultListCellFactory, ResourceBundle resourceBundle) {
|
||||
public CheckDetailController(ObjectProperty<Check> selectedTask, ResultListCellFactory resultListCellFactory) {
|
||||
this.resultListCellFactory = resultListCellFactory;
|
||||
this.resourceBundle = resourceBundle;
|
||||
this.results = EasyBind.wrapList(FXCollections.observableArrayList());
|
||||
this.taskState = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::stateProperty);
|
||||
this.taskName = EasyBind.wrapNullable(selectedTask).map(HealthCheckTask::getTitle).orElse("");
|
||||
this.taskDuration = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::durationInMillisProperty).orElse(-1L).map(this::millisToReadAbleDuration);
|
||||
this.taskRunning = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::runningProperty).orElse(false); //TODO: DOES NOT WORK
|
||||
this.taskScheduled = taskState.map(Worker.State.SCHEDULED::equals).orElse(false);
|
||||
this.taskNotStarted = taskState.map(Worker.State.READY::equals).orElse(false);
|
||||
this.taskSucceeded = taskState.map(Worker.State.SUCCEEDED::equals).orElse(false);
|
||||
this.taskFailed = taskState.map(Worker.State.FAILED::equals).orElse(false);
|
||||
this.taskCancelled = taskState.map(Worker.State.CANCELLED::equals).orElse(false);
|
||||
this.taskFinished = EasyBind.combine(taskSucceeded, taskFailed, taskCancelled, (a, b, c) -> a || b || c);
|
||||
this.check = selectedTask;
|
||||
this.checkState = EasyBind.wrapNullable(selectedTask).mapObservable(Check::stateProperty);
|
||||
this.checkName = EasyBind.wrapNullable(selectedTask).map(Check::getName).orElse("");
|
||||
this.checkRunning = checkState.map(Check.CheckState.RUNNING::equals).orElse(false);
|
||||
this.checkScheduled = checkState.map(Check.CheckState.SCHEDULED::equals).orElse(false);
|
||||
this.checkSkipped = checkState.map(Check.CheckState.SKIPPED::equals).orElse(false);
|
||||
this.checkSucceeded = checkState.map(Check.CheckState.SUCCEEDED::equals).orElse(false);
|
||||
this.checkFailed = checkState.map(Check.CheckState.ERROR::equals).orElse(false);
|
||||
this.checkCancelled = checkState.map(Check.CheckState.CANCELLED::equals).orElse(false);
|
||||
this.checkFinished = EasyBind.combine(checkSucceeded, checkFailed, checkCancelled, (a, b, c) -> a || b || c);
|
||||
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) );
|
||||
selectedTask.addListener(this::selectedTaskChanged);
|
||||
}
|
||||
|
||||
private void selectedTaskChanged(ObservableValue<? extends HealthCheckTask> observable, HealthCheckTask oldValue, HealthCheckTask newValue) {
|
||||
private void selectedTaskChanged(ObservableValue<? extends Check> observable, Check oldValue, Check newValue) {
|
||||
if (resultSubscription != null) {
|
||||
resultSubscription.unsubscribe();
|
||||
}
|
||||
if (newValue != null) {
|
||||
resultSubscription = EasyBind.bindContent(results, newValue.results());
|
||||
resultSubscription = EasyBind.bindContent(results, newValue.getResults());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,20 +80,12 @@ public class CheckDetailController implements FxController {
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public String getTaskName() {
|
||||
return taskName.getValue();
|
||||
public String getCheckName() {
|
||||
return checkName.getValue();
|
||||
}
|
||||
|
||||
public Binding<String> taskNameProperty() {
|
||||
return taskName;
|
||||
}
|
||||
|
||||
public String getTaskDuration() {
|
||||
return taskDuration.getValue();
|
||||
}
|
||||
|
||||
public Binding<String> taskDurationProperty() {
|
||||
return taskDuration;
|
||||
public Binding<String> checkNameProperty() {
|
||||
return checkName;
|
||||
}
|
||||
|
||||
public long getCountOfWarnSeverity() {
|
||||
@@ -115,77 +104,75 @@ public class CheckDetailController implements FxController {
|
||||
return countOfCritSeverity;
|
||||
}
|
||||
|
||||
public boolean isTaskRunning() {
|
||||
return taskRunning.getValue();
|
||||
public boolean isCheckRunning() {
|
||||
return checkRunning.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> taskRunningProperty() {
|
||||
return taskRunning;
|
||||
public Binding<Boolean> checkRunningProperty() {
|
||||
return checkRunning;
|
||||
}
|
||||
|
||||
public boolean isTaskFinished() {
|
||||
return taskFinished.getValue();
|
||||
public boolean isCheckFinished() {
|
||||
return checkFinished.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> taskFinishedProperty() {
|
||||
return taskFinished;
|
||||
public Binding<Boolean> checkFinishedProperty() {
|
||||
return checkFinished;
|
||||
}
|
||||
|
||||
public boolean isTaskScheduled() {
|
||||
return taskScheduled.getValue();
|
||||
public boolean isCheckScheduled() {
|
||||
return checkScheduled.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> taskScheduledProperty() {
|
||||
return taskScheduled;
|
||||
public Binding<Boolean> checkScheduledProperty() {
|
||||
return checkScheduled;
|
||||
}
|
||||
|
||||
public boolean isTaskNotStarted() {
|
||||
return taskNotStarted.getValue();
|
||||
public boolean isCheckSkipped() {
|
||||
return checkSkipped.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> taskNotStartedProperty() {
|
||||
return taskNotStarted;
|
||||
public Binding<Boolean> checkSkippedProperty() {
|
||||
return checkSkipped;
|
||||
}
|
||||
|
||||
public boolean isTaskSucceeded() {
|
||||
return taskSucceeded.getValue();
|
||||
public boolean isCheckSucceeded() {
|
||||
return checkSucceeded.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> taskSucceededProperty() {
|
||||
return taskSucceeded;
|
||||
public Binding<Boolean> checkSucceededProperty() {
|
||||
return checkSucceeded;
|
||||
}
|
||||
|
||||
public boolean isTaskFailed() {
|
||||
return taskFailed.getValue();
|
||||
public boolean isCheckFailed() {
|
||||
return checkFailed.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> taskFailedProperty() {
|
||||
return taskFailed;
|
||||
public Binding<Boolean> checkFailedProperty() {
|
||||
return checkFailed;
|
||||
}
|
||||
|
||||
public boolean isTaskCancelled() {
|
||||
return taskCancelled.getValue();
|
||||
public boolean isCheckCancelled() {
|
||||
return checkCancelled.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> taskCancelledProperty() {
|
||||
return taskCancelled;
|
||||
public Binding<Boolean> warnOrCritsExistProperty() {
|
||||
return warnOrCritsExist;
|
||||
}
|
||||
|
||||
private String millisToReadAbleDuration(Number millis) {
|
||||
Duration tmp = Duration.ofMillis(millis.longValue());
|
||||
long hours = tmp.toHoursPart();
|
||||
long minutes = tmp.toMinutesPart();
|
||||
long seconds = tmp.toSecondsPart();
|
||||
if (hours != 0) {
|
||||
String hms_format = resourceBundle.getString("health.check.detail.hmsFormat");
|
||||
return String.format(hms_format, hours, minutes, seconds);
|
||||
} else if (minutes != 0) {
|
||||
String ms_format = resourceBundle.getString("health.check.detail.msFormat");
|
||||
return String.format(ms_format, minutes, seconds);
|
||||
} else {
|
||||
String s_format = resourceBundle.getString("health.check.detail.sFormat");
|
||||
return String.format(s_format, seconds);
|
||||
}
|
||||
public boolean isWarnOrCritsExist() {
|
||||
return warnOrCritsExist.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> checkCancelledProperty() {
|
||||
return checkCancelled;
|
||||
}
|
||||
|
||||
public ObjectProperty<Check> checkProperty() {
|
||||
return check;
|
||||
}
|
||||
|
||||
public Check getCheck() {
|
||||
return check.get();
|
||||
}
|
||||
}
|
||||
|
||||
109
src/main/java/org/cryptomator/ui/health/CheckExecutor.java
Normal file
109
src/main/java/org/cryptomator/ui/health/CheckExecutor.java
Normal file
@@ -0,0 +1,109 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import com.google.common.collect.Comparators;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptofs.VaultConfig;
|
||||
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||
import org.cryptomator.cryptolib.api.CryptorProvider;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.application.Platform;
|
||||
import javafx.concurrent.Task;
|
||||
import java.nio.file.Path;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.BlockingDeque;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.LinkedBlockingDeque;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@HealthCheckScoped
|
||||
public class CheckExecutor {
|
||||
|
||||
private final Path vaultPath;
|
||||
private final SecureRandom csprng;
|
||||
private final Masterkey masterkey;
|
||||
private final VaultConfig vaultConfig;
|
||||
private final ExecutorService sequentialExecutor;
|
||||
private final BlockingDeque<CheckTask> tasksToExecute;
|
||||
|
||||
|
||||
@Inject
|
||||
public CheckExecutor(@HealthCheckWindow Vault vault, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, SecureRandom csprng) {
|
||||
this.vaultPath = vault.getPath();
|
||||
this.masterkey = masterkeyRef.get();
|
||||
this.vaultConfig = vaultConfigRef.get();
|
||||
this.csprng = csprng;
|
||||
this.tasksToExecute = new LinkedBlockingDeque<>();
|
||||
this.sequentialExecutor = Executors.newSingleThreadExecutor();
|
||||
}
|
||||
|
||||
public synchronized void executeBatch(List<Check> checks) {
|
||||
checks.stream().map(c -> {
|
||||
c.setState(Check.CheckState.SCHEDULED);
|
||||
var task = new CheckTask(c);
|
||||
tasksToExecute.addLast(task);
|
||||
return task;
|
||||
}).forEach(sequentialExecutor::submit);
|
||||
}
|
||||
|
||||
public synchronized void cancel() {
|
||||
CheckTask task;
|
||||
while ((task = tasksToExecute.pollLast()) != null) {
|
||||
task.cancel(true);
|
||||
}
|
||||
}
|
||||
|
||||
private class CheckTask extends Task<Void> {
|
||||
|
||||
private final Check c;
|
||||
private DiagnosticResult.Severity highestResultSeverity = DiagnosticResult.Severity.GOOD;
|
||||
|
||||
CheckTask(Check c) {
|
||||
this.c = c;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void call() throws Exception {
|
||||
try (var masterkeyClone = masterkey.clone(); //
|
||||
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)));
|
||||
highestResultSeverity = Comparators.max(highestResultSeverity, diagnosis.getSeverity());
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void running() {
|
||||
c.setState(Check.CheckState.RUNNING);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cancelled() {
|
||||
c.setState(Check.CheckState.CANCELLED);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void succeeded() {
|
||||
c.setState(Check.CheckState.SUCCEEDED);
|
||||
c.setHighestResultSeverity(highestResultSeverity);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void failed() {
|
||||
c.setState(Check.CheckState.ERROR);
|
||||
c.setError(this.getException());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done() {
|
||||
tasksToExecute.remove(this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||
import org.cryptomator.ui.controls.FontAwesome5Icon;
|
||||
import org.cryptomator.ui.controls.FontAwesome5IconView;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.ListCell;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
class CheckListCell extends ListCell<HealthCheckTask> {
|
||||
|
||||
private final FontAwesome5IconView stateIcon = new FontAwesome5IconView();
|
||||
private CheckBox checkBox = new CheckBox();
|
||||
|
||||
CheckListCell() {
|
||||
setPadding(new Insets(6));
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
setContentDisplay(ContentDisplay.LEFT);
|
||||
getStyleClass().add("label");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(HealthCheckTask item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (item != null) {
|
||||
setText(item.getTitle());
|
||||
graphicProperty().bind(Bindings.createObjectBinding(() -> graphicForState(item.getState()), item.stateProperty()));
|
||||
stateIcon.glyphProperty().bind(Bindings.createObjectBinding(() -> glyphForState(item), item.stateProperty()));
|
||||
checkBox.selectedProperty().bindBidirectional(item.chosenForExecutionProperty());
|
||||
} else {
|
||||
graphicProperty().unbind();
|
||||
setGraphic(null);
|
||||
setText(null);
|
||||
checkBox.selectedProperty().unbind();
|
||||
}
|
||||
}
|
||||
|
||||
private Node graphicForState(Worker.State state) {
|
||||
return switch (state) {
|
||||
case READY -> checkBox;
|
||||
case SCHEDULED, RUNNING, FAILED, CANCELLED, SUCCEEDED -> stateIcon;
|
||||
};
|
||||
}
|
||||
|
||||
private FontAwesome5Icon glyphForState(HealthCheckTask item) {
|
||||
return switch (item.getState()) {
|
||||
case READY -> FontAwesome5Icon.COG; //just a placeholder
|
||||
case SCHEDULED -> FontAwesome5Icon.CLOCK;
|
||||
case RUNNING -> FontAwesome5Icon.SPINNER;
|
||||
case FAILED -> FontAwesome5Icon.EXCLAMATION_TRIANGLE;
|
||||
case CANCELLED -> FontAwesome5Icon.BAN;
|
||||
case SUCCEEDED -> checkFoundProblems(item) ? FontAwesome5Icon.EXCLAMATION_TRIANGLE : FontAwesome5Icon.CHECK;
|
||||
};
|
||||
}
|
||||
|
||||
private boolean checkFoundProblems(HealthCheckTask item) {
|
||||
Predicate<DiagnosticResult.Severity> isProblem = severity -> switch (severity) {
|
||||
case WARN, CRITICAL -> true;
|
||||
case INFO, GOOD -> false;
|
||||
};
|
||||
return item.results().stream().map(Result::diagnosis).map(DiagnosticResult::getSeverity).anyMatch(isProblem);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import com.tobiasdiez.easybind.EasyBind;
|
||||
import com.tobiasdiez.easybind.Subscription;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class CheckListCellController implements FxController {
|
||||
|
||||
|
||||
private final ObjectProperty<Check> check;
|
||||
private final Binding<String> checkName;
|
||||
private final Binding<Boolean> checkRunnable;
|
||||
private final List<Subscription> subscriptions;
|
||||
|
||||
/* FXML */
|
||||
public CheckBox forRunSelectedCheckBox;
|
||||
|
||||
@Inject
|
||||
public CheckListCellController() {
|
||||
check = new SimpleObjectProperty<>();
|
||||
checkRunnable = EasyBind.wrapNullable(check).mapObservable(Check::stateProperty).map(Check.CheckState.RUNNABLE::equals).orElse(false);
|
||||
checkName = EasyBind.wrapNullable(check).map(Check::getName).orElse("");
|
||||
subscriptions = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
subscriptions.add(EasyBind.subscribe(check, c -> {
|
||||
forRunSelectedCheckBox.selectedProperty().unbind();
|
||||
if (c != null) {
|
||||
forRunSelectedCheckBox.selectedProperty().bindBidirectional(c.chosenForExecutionProperty());
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public ObjectProperty<Check> checkProperty() {
|
||||
return check;
|
||||
}
|
||||
|
||||
public Check getCheck() {
|
||||
return check.get();
|
||||
}
|
||||
|
||||
public void setCheck(Check c) {
|
||||
check.set(c);
|
||||
}
|
||||
|
||||
public Binding<String> checkNameProperty() {
|
||||
return checkName;
|
||||
}
|
||||
|
||||
public String getCheckName() {
|
||||
return checkName.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> checkRunnableProperty() {
|
||||
return checkRunnable;
|
||||
}
|
||||
|
||||
public boolean isCheckRunnable() {
|
||||
return checkRunnable.getValue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import org.cryptomator.ui.common.FxmlLoaderFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.util.Callback;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
|
||||
// unscoped because each cell needs its own controller
|
||||
public class CheckListCellFactory implements Callback<ListView<Check>, ListCell<Check>> {
|
||||
|
||||
private final FxmlLoaderFactory fxmlLoaders;
|
||||
|
||||
@Inject
|
||||
CheckListCellFactory(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) {
|
||||
this.fxmlLoaders = fxmlLoaders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListCell<Check> call(ListView<Check> param) {
|
||||
try {
|
||||
FXMLLoader fxmlLoader = fxmlLoaders.load("/fxml/health_check_listcell.fxml");
|
||||
return new CheckListCellFactory.Cell(fxmlLoader.getRoot(), fxmlLoader.getController());
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Failed to load /fxml/health_check_listcell.fxml.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Cell extends ListCell<Check> {
|
||||
|
||||
private final Parent node;
|
||||
private final CheckListCellController controller;
|
||||
|
||||
public Cell(Parent node, CheckListCellController controller) {
|
||||
this.node = node;
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Check item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (item == null || empty) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
controller.setCheck(item);
|
||||
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
|
||||
setGraphic(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.tobiasdiez.easybind.EasyBind;
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
@@ -10,110 +8,100 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.binding.Binding;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.IntegerBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.transformation.FilteredList;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.SelectionMode;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
@HealthCheckScoped
|
||||
public class CheckListController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CheckListController.class);
|
||||
private static final Set<Worker.State> END_STATES = Set.of(Worker.State.FAILED, Worker.State.CANCELLED, Worker.State.SUCCEEDED);
|
||||
|
||||
private final Stage window;
|
||||
private final ObservableList<HealthCheckTask> tasks;
|
||||
private final FilteredList<HealthCheckTask> chosenTasks;
|
||||
private final ObservableList<Check> checks;
|
||||
private final CheckExecutor checkExecutor;
|
||||
private final FilteredList<Check> chosenChecks;
|
||||
private final ReportWriter reportWriter;
|
||||
private final ExecutorService executorService;
|
||||
private final ObjectProperty<HealthCheckTask> selectedTask;
|
||||
private final ObjectProperty<Check> selectedCheck;
|
||||
private final BooleanBinding mainRunStarted; //TODO: rerunning not considered for now
|
||||
private final BooleanBinding somethingsRunning;
|
||||
private final Lazy<ErrorComponent.Builder> errorComponentBuilder;
|
||||
private final SimpleObjectProperty<Worker<?>> runningTask;
|
||||
private final Binding<Boolean> running;
|
||||
private final Binding<Boolean> finished;
|
||||
private final IntegerBinding chosenTaskCount;
|
||||
private final BooleanBinding anyCheckSelected;
|
||||
private final BooleanProperty showResultScreen;
|
||||
private final CheckListCellFactory listCellFactory;
|
||||
|
||||
/* FXML */
|
||||
public ListView<HealthCheckTask> checksListView;
|
||||
public ListView<Check> checksListView;
|
||||
|
||||
@Inject
|
||||
public CheckListController(@HealthCheckWindow Stage window, Lazy<List<HealthCheckTask>> tasks, ReportWriter reportWriteTask, ObjectProperty<HealthCheckTask> selectedTask, ExecutorService executorService, Lazy<ErrorComponent.Builder> errorComponentBuilder) {
|
||||
public CheckListController(@HealthCheckWindow Stage window, List<Check> checks, CheckExecutor checkExecutor, ReportWriter reportWriteTask, ObjectProperty<Check> selectedCheck, Lazy<ErrorComponent.Builder> errorComponentBuilder, CheckListCellFactory listCellFactory) {
|
||||
this.window = window;
|
||||
this.tasks = FXCollections.observableList(tasks.get(), HealthCheckTask::observables);
|
||||
this.chosenTasks = this.tasks.filtered(HealthCheckTask::isChosenForExecution);
|
||||
this.checks = FXCollections.observableList(checks, Check::observables);
|
||||
this.checkExecutor = checkExecutor;
|
||||
this.listCellFactory = listCellFactory;
|
||||
this.chosenChecks = this.checks.filtered(Check::isChosenForExecution);
|
||||
this.reportWriter = reportWriteTask;
|
||||
this.executorService = executorService;
|
||||
this.selectedTask = selectedTask;
|
||||
this.selectedCheck = selectedCheck;
|
||||
this.errorComponentBuilder = errorComponentBuilder;
|
||||
this.runningTask = new SimpleObjectProperty<>();
|
||||
this.running = EasyBind.wrapNullable(runningTask).mapObservable(Worker::runningProperty).orElse(false);
|
||||
this.finished = EasyBind.wrapNullable(runningTask).mapObservable(Worker::stateProperty).map(END_STATES::contains).orElse(false);
|
||||
this.chosenTaskCount = Bindings.size(this.chosenTasks);
|
||||
this.anyCheckSelected = selectedTask.isNotNull();
|
||||
this.showResultScreen = new SimpleBooleanProperty(false);
|
||||
this.chosenTaskCount = Bindings.size(this.chosenChecks);
|
||||
this.mainRunStarted = Bindings.isEmpty(this.checks.filtered(c -> c.getState() == Check.CheckState.RUNNABLE));
|
||||
this.somethingsRunning = Bindings.isNotEmpty(this.checks.filtered(c -> c.getState() == Check.CheckState.SCHEDULED || c.getState() == Check.CheckState.RUNNING));
|
||||
this.anyCheckSelected = selectedCheck.isNotNull();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
checksListView.setItems(tasks);
|
||||
checksListView.setCellFactory(view -> new CheckListCell());
|
||||
selectedTask.bind(checksListView.getSelectionModel().selectedItemProperty());
|
||||
checksListView.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
||||
checksListView.setItems(checks);
|
||||
checksListView.setCellFactory(listCellFactory);
|
||||
selectedCheck.bind(checksListView.getSelectionModel().selectedItemProperty());
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void toggleSelectAll(ActionEvent event) {
|
||||
if (event.getSource() instanceof CheckBox c) {
|
||||
tasks.forEach(t -> t.chosenForExecutionProperty().set(c.isSelected()));
|
||||
}
|
||||
public void selectAllChecks() {
|
||||
checks.forEach(t -> t.chosenForExecutionProperty().set(true));
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void deselectAllChecks() {
|
||||
checks.forEach(t -> t.chosenForExecutionProperty().set(false));
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void runSelectedChecks() {
|
||||
Preconditions.checkState(runningTask.get() == null);
|
||||
Preconditions.checkState(!mainRunStarted.get());
|
||||
Preconditions.checkState(!somethingsRunning.get());
|
||||
Preconditions.checkState(!chosenChecks.isEmpty());
|
||||
|
||||
// prevent further interaction by cancelling non-chosen tasks:
|
||||
tasks.filtered(Predicates.not(chosenTasks::contains)).forEach(HealthCheckTask::cancel);
|
||||
|
||||
// run chosen tasks:
|
||||
var batchService = new BatchService(chosenTasks);
|
||||
batchService.setExecutor(executorService);
|
||||
batchService.start();
|
||||
runningTask.set(batchService);
|
||||
showResultScreen.set(true);
|
||||
checksListView.getSelectionModel().select(chosenTasks.get(0));
|
||||
checks.filtered(c -> !c.isChosenForExecution()).forEach(c -> c.setState(Check.CheckState.SKIPPED));
|
||||
checkExecutor.executeBatch(chosenChecks);
|
||||
checksListView.getSelectionModel().select(chosenChecks.get(0));
|
||||
checksListView.refresh();
|
||||
window.sizeToScene();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public synchronized void cancelCheck() {
|
||||
Preconditions.checkState(runningTask.get() != null);
|
||||
runningTask.get().cancel();
|
||||
public synchronized void cancelRun() {
|
||||
Preconditions.checkState(somethingsRunning.get());
|
||||
checkExecutor.cancel();
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void exportResults() {
|
||||
try {
|
||||
reportWriter.writeReport(tasks);
|
||||
reportWriter.writeReport(chosenChecks);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to write health check report.", e);
|
||||
errorComponentBuilder.get().cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
|
||||
@@ -122,19 +110,11 @@ public class CheckListController implements FxController {
|
||||
|
||||
/* Getter/Setter */
|
||||
public boolean isRunning() {
|
||||
return running.getValue();
|
||||
return somethingsRunning.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> runningProperty() {
|
||||
return running;
|
||||
}
|
||||
|
||||
public boolean isFinished() {
|
||||
return finished.getValue();
|
||||
}
|
||||
|
||||
public Binding<Boolean> finishedProperty() {
|
||||
return finished;
|
||||
public BooleanBinding runningProperty() {
|
||||
return somethingsRunning;
|
||||
}
|
||||
|
||||
public boolean isAnyCheckSelected() {
|
||||
@@ -145,12 +125,12 @@ public class CheckListController implements FxController {
|
||||
return anyCheckSelected;
|
||||
}
|
||||
|
||||
public boolean getShowResultScreen() {
|
||||
return showResultScreen.get();
|
||||
public boolean isMainRunStarted() {
|
||||
return mainRunStarted.get();
|
||||
}
|
||||
|
||||
public BooleanProperty showResultScreenProperty() {
|
||||
return showResultScreen;
|
||||
public BooleanBinding mainRunStartedProperty() {
|
||||
return mainRunStarted;
|
||||
}
|
||||
|
||||
public int getChosenTaskCount() {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import com.tobiasdiez.easybind.EasyBind;
|
||||
import com.tobiasdiez.easybind.Subscription;
|
||||
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||
import org.cryptomator.ui.controls.FontAwesome5Icon;
|
||||
import org.cryptomator.ui.controls.FontAwesome5IconView;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableObjectValue;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A {@link FontAwesome5IconView} that automatically sets the glyph depending on
|
||||
* the {@link Check#stateProperty() state} and {@link Check#highestResultSeverityProperty() severity} of a HealthCheck.
|
||||
*/
|
||||
public class CheckStateIconView extends FontAwesome5IconView {
|
||||
|
||||
private final ObjectProperty<Check> check = new SimpleObjectProperty<>();
|
||||
private final ObservableObjectValue<Check.CheckState> state;
|
||||
private final ObservableObjectValue<DiagnosticResult.Severity> severity;
|
||||
private final List<Subscription> subscriptions;
|
||||
|
||||
public CheckStateIconView() {
|
||||
this.state = EasyBind.wrapNullable(check).mapObservable(Check::stateProperty).asOrdinary();
|
||||
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-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)) //
|
||||
);
|
||||
}
|
||||
|
||||
private FontAwesome5Icon glyphForState() {
|
||||
if (state.getValue() == null) {
|
||||
return null;
|
||||
}
|
||||
return switch (state.getValue()) {
|
||||
case RUNNABLE -> null;
|
||||
case SKIPPED -> FontAwesome5Icon.FAST_FORWARD;
|
||||
case SCHEDULED -> FontAwesome5Icon.CLOCK;
|
||||
case RUNNING -> FontAwesome5Icon.SPINNER;
|
||||
case ERROR -> FontAwesome5Icon.TIMES;
|
||||
case CANCELLED -> FontAwesome5Icon.BAN;
|
||||
case SUCCEEDED -> glyphIconForSeverity();
|
||||
};
|
||||
}
|
||||
|
||||
private FontAwesome5Icon glyphIconForSeverity() {
|
||||
if (severity.getValue() == null) {
|
||||
return null;
|
||||
}
|
||||
return switch (severity.getValue()) {
|
||||
case GOOD, INFO -> FontAwesome5Icon.CHECK;
|
||||
case WARN, CRITICAL -> FontAwesome5Icon.EXCLAMATION_TRIANGLE;
|
||||
};
|
||||
}
|
||||
|
||||
public ObjectProperty<Check> checkProperty() {
|
||||
return check;
|
||||
}
|
||||
|
||||
public void setCheck(Check c) {
|
||||
check.set(c);
|
||||
}
|
||||
|
||||
public Check getCheck() {
|
||||
return check.get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import org.cryptomator.cryptofs.VaultConfig;
|
||||
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||
import org.cryptomator.cryptofs.health.api.HealthCheck;
|
||||
import org.cryptomator.cryptolib.api.Cryptor;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* FIXME: Remove in production release
|
||||
*/
|
||||
public class DummyHealthChecks {
|
||||
|
||||
public static class DummyCheck1 implements HealthCheck {
|
||||
|
||||
@Override
|
||||
public void check(Path path, VaultConfig vaultConfig, Masterkey masterkey, Cryptor cryptor, Consumer<DiagnosticResult> consumer) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
public static class DummyCheck2 implements HealthCheck {
|
||||
|
||||
@Override
|
||||
public void check(Path path, VaultConfig vaultConfig, Masterkey masterkey, Cryptor cryptor, Consumer<DiagnosticResult> consumer) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
public static class DummyCheck3 implements HealthCheck {
|
||||
|
||||
@Override
|
||||
public void check(Path path, VaultConfig vaultConfig, Masterkey masterkey, Cryptor cryptor, Consumer<DiagnosticResult> consumer) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import dagger.BindsInstance;
|
||||
import dagger.Lazy;
|
||||
import dagger.Subcomponent;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptofs.VaultConfig;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
|
||||
@@ -15,20 +16,27 @@ import javafx.stage.Stage;
|
||||
@Subcomponent(modules = {HealthCheckModule.class})
|
||||
public interface HealthCheckComponent {
|
||||
|
||||
LoadUnverifiedConfigResult loadConfig();
|
||||
|
||||
@HealthCheckWindow
|
||||
Stage window();
|
||||
|
||||
@Named("windowToClose")
|
||||
Stage windowToClose();
|
||||
|
||||
@FxmlScene(FxmlFile.HEALTH_START)
|
||||
Lazy<Scene> scene();
|
||||
Lazy<Scene> startScene();
|
||||
|
||||
@FxmlScene(FxmlFile.HEALTH_START_FAIL)
|
||||
Lazy<Scene> failScene();
|
||||
|
||||
default Stage showHealthCheckWindow() {
|
||||
Stage stage = window();
|
||||
stage.setScene(scene().get());
|
||||
// TODO reevaluate config loading, as soon as we have the new generic error screen
|
||||
var unverifiedConf = loadConfig();
|
||||
if (unverifiedConf.config() != null) {
|
||||
stage.setScene(startScene().get());
|
||||
} else {
|
||||
stage.setScene(failScene().get());
|
||||
}
|
||||
stage.show();
|
||||
windowToClose().close();
|
||||
return stage;
|
||||
}
|
||||
|
||||
@@ -39,9 +47,10 @@ public interface HealthCheckComponent {
|
||||
Builder vault(@HealthCheckWindow Vault vault);
|
||||
|
||||
@BindsInstance
|
||||
Builder windowToClose(@Named("windowToClose") Stage window);
|
||||
Builder owner(@Named("healthCheckOwner") Stage owner);
|
||||
|
||||
HealthCheckComponent build();
|
||||
}
|
||||
|
||||
record LoadUnverifiedConfigResult(VaultConfig.UnverifiedVaultConfig config, Throwable error) {}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.StageFactory;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||
import org.cryptomator.ui.mainwindow.MainWindow;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
@@ -26,8 +26,8 @@ import javafx.beans.value.ChangeListener;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Collection;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -37,6 +37,18 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
@Module(subcomponents = {KeyLoadingComponent.class})
|
||||
abstract class HealthCheckModule {
|
||||
|
||||
// TODO reevaluate config loading, as soon as we have the new generic error screen
|
||||
@Provides
|
||||
@HealthCheckScoped
|
||||
static HealthCheckComponent.LoadUnverifiedConfigResult provideLoadConfigResult(@HealthCheckWindow Vault vault) {
|
||||
try {
|
||||
return new HealthCheckComponent.LoadUnverifiedConfigResult(vault.getUnverifiedVaultConfig(), null);
|
||||
} catch (IOException e) {
|
||||
return new HealthCheckComponent.LoadUnverifiedConfigResult(null, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Provides
|
||||
@HealthCheckScoped
|
||||
static AtomicReference<Masterkey> provideMasterkeyRef() {
|
||||
@@ -51,27 +63,20 @@ abstract class HealthCheckModule {
|
||||
|
||||
@Provides
|
||||
@HealthCheckScoped
|
||||
static Collection<HealthCheck> provideAvailableHealthChecks() {
|
||||
return HealthCheck.allChecks();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@HealthCheckScoped
|
||||
static ObjectProperty<HealthCheckTask> provideSelectedHealthCheckTask() {
|
||||
static ObjectProperty<Check> provideSelectedCheck() {
|
||||
return new SimpleObjectProperty<>();
|
||||
}
|
||||
|
||||
/* Only inject with Lazy-Wrapper!*/
|
||||
@Provides
|
||||
@HealthCheckScoped
|
||||
static List<HealthCheckTask> provideAvailableHealthCheckTasks(Collection<HealthCheck> availableHealthChecks, @HealthCheckWindow Vault vault, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, SecureRandom csprng, ResourceBundle resourceBundle) {
|
||||
return availableHealthChecks.stream().map(check -> new HealthCheckTask(vault.getPath(), vaultConfigRef.get(), masterkeyRef.get(), csprng, check, resourceBundle)).toList();
|
||||
static List<Check> provideAvailableChecks() {
|
||||
return HealthCheck.allChecks().stream().map(Check::new).toList();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@HealthCheckWindow
|
||||
@HealthCheckScoped
|
||||
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @HealthCheckWindow Vault vault, @HealthCheckWindow Stage window) {
|
||||
static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @HealthCheckWindow Vault vault, @Named("unlockWindow") Stage window ) {
|
||||
return compBuilder.vault(vault).window(window).build().keyloadingStrategy();
|
||||
}
|
||||
|
||||
@@ -82,14 +87,26 @@ abstract class HealthCheckModule {
|
||||
return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("unlockWindow")
|
||||
@HealthCheckScoped
|
||||
static Stage provideUnlockWindow (@HealthCheckWindow Stage window, @HealthCheckWindow Vault vault, StageFactory factory, ResourceBundle resourceBundle) {
|
||||
Stage stage = factory.create();
|
||||
stage.initModality(Modality.WINDOW_MODAL);
|
||||
stage.initOwner(window);
|
||||
stage.setTitle(String.format(resourceBundle.getString("unlock.title"), vault.getDisplayName()));
|
||||
stage.setResizable(false);
|
||||
return stage;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@HealthCheckWindow
|
||||
@HealthCheckScoped
|
||||
static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle, ChangeListener<Boolean> showingListener) {
|
||||
static Stage provideStage(StageFactory factory, @Named("healthCheckOwner") Stage owner, @HealthCheckWindow Vault vault, ChangeListener<Boolean> showingListener, ResourceBundle resourceBundle) {
|
||||
Stage stage = factory.create();
|
||||
stage.initModality(Modality.WINDOW_MODAL);
|
||||
stage.initOwner(owner);
|
||||
stage.setTitle(resourceBundle.getString("health.title"));
|
||||
stage.setTitle(String.format(resourceBundle.getString("health.title"), vault.getDisplayName()));
|
||||
stage.setResizable(true);
|
||||
stage.showingProperty().addListener(showingListener); // bind masterkey lifecycle to window
|
||||
return stage;
|
||||
@@ -112,6 +129,13 @@ abstract class HealthCheckModule {
|
||||
return fxmlLoaders.createScene(FxmlFile.HEALTH_START);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.HEALTH_START_FAIL)
|
||||
@HealthCheckScoped
|
||||
static Scene provideHealthStartFailScene(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) {
|
||||
return fxmlLoaders.createScene(FxmlFile.HEALTH_START_FAIL);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FxmlScene(FxmlFile.HEALTH_CHECK_LIST)
|
||||
@HealthCheckScoped
|
||||
@@ -124,6 +148,11 @@ abstract class HealthCheckModule {
|
||||
@FxControllerKey(StartController.class)
|
||||
abstract FxController bindStartController(StartController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(StartFailController.class)
|
||||
abstract FxController bindStartFailController(StartFailController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(CheckListController.class)
|
||||
@@ -139,4 +168,8 @@ abstract class HealthCheckModule {
|
||||
@FxControllerKey(ResultListCellController.class)
|
||||
abstract FxController bindResultListCellController(ResultListCellController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(CheckListCellController.class)
|
||||
abstract FxController bindCheckListCellController(CheckListCellController controller);
|
||||
}
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import org.cryptomator.cryptofs.VaultConfig;
|
||||
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||
import org.cryptomator.cryptofs.health.api.HealthCheck;
|
||||
import org.cryptomator.cryptolib.api.CryptorProvider;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.LongProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleLongProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.concurrent.Task;
|
||||
import java.nio.file.Path;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.Objects;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
class HealthCheckTask extends Task<Void> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HealthCheckTask.class);
|
||||
|
||||
private final Path vaultPath;
|
||||
private final VaultConfig vaultConfig;
|
||||
private final Masterkey masterkey;
|
||||
private final SecureRandom csprng;
|
||||
private final HealthCheck check;
|
||||
private final ObservableList<Result> results;
|
||||
private final LongProperty durationInMillis;
|
||||
private final BooleanProperty chosenForExecution;
|
||||
|
||||
public HealthCheckTask(Path vaultPath, VaultConfig vaultConfig, Masterkey masterkey, SecureRandom csprng, HealthCheck check, ResourceBundle resourceBundle) {
|
||||
this.vaultPath = Objects.requireNonNull(vaultPath);
|
||||
this.vaultConfig = Objects.requireNonNull(vaultConfig);
|
||||
this.masterkey = Objects.requireNonNull(masterkey);
|
||||
this.csprng = Objects.requireNonNull(csprng);
|
||||
this.check = Objects.requireNonNull(check);
|
||||
this.results = FXCollections.observableArrayList(Result::observables);
|
||||
try {
|
||||
updateTitle(resourceBundle.getString("health." + check.name()));
|
||||
} catch (MissingResourceException e) {
|
||||
LOG.warn("Missing proper name for health check {}, falling back to default.", check.name());
|
||||
updateTitle(check.name());
|
||||
}
|
||||
this.durationInMillis = new SimpleLongProperty(-1);
|
||||
this.chosenForExecution = new SimpleBooleanProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void call() {
|
||||
Instant start = Instant.now();
|
||||
try (var masterkeyClone = masterkey.clone(); //
|
||||
var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) {
|
||||
check.check(vaultPath, vaultConfig, masterkeyClone, cryptor, diagnosis -> {
|
||||
if (isCancelled()) {
|
||||
throw new CancellationException();
|
||||
}
|
||||
Platform.runLater(() -> results.add(Result.create(diagnosis)));
|
||||
});
|
||||
}
|
||||
Platform.runLater(() -> durationInMillis.set(Duration.between(start, Instant.now()).toMillis()));
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void scheduled() {
|
||||
LOG.info("starting {}", check.name());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done() {
|
||||
LOG.info("finished {}", check.name());
|
||||
}
|
||||
|
||||
/* Getter */
|
||||
|
||||
Observable[] observables() {
|
||||
return new Observable[]{results, chosenForExecution};
|
||||
}
|
||||
|
||||
public ObservableList<Result> results() {
|
||||
return results;
|
||||
}
|
||||
|
||||
public HealthCheck getCheck() {
|
||||
return check;
|
||||
}
|
||||
|
||||
public LongProperty durationInMillisProperty() {
|
||||
return durationInMillis;
|
||||
}
|
||||
|
||||
public long getDurationInMillis() {
|
||||
return durationInMillis.get();
|
||||
}
|
||||
|
||||
public BooleanProperty chosenForExecutionProperty() {
|
||||
return chosenForExecution;
|
||||
}
|
||||
|
||||
public boolean isChosenForExecution() {
|
||||
return chosenForExecution.get();
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,12 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import com.google.common.base.Throwables;
|
||||
import org.cryptomator.common.Environment;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptofs.VaultConfig;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.application.Application;
|
||||
import javafx.concurrent.Worker;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
@@ -28,11 +25,10 @@ import java.util.stream.Collectors;
|
||||
@HealthCheckScoped
|
||||
public class ReportWriter {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ReportWriter.class);
|
||||
private static final String REPORT_HEADER = """
|
||||
**************************************
|
||||
* Cryptomator Vault Health Report *
|
||||
**************************************
|
||||
*******************************************
|
||||
* Cryptomator Vault Health Report *
|
||||
*******************************************
|
||||
Analyzed vault: %s (Current name "%s")
|
||||
Vault storage path: %s
|
||||
""";
|
||||
@@ -58,38 +54,35 @@ public class ReportWriter {
|
||||
this.exportDestination = env.getLogDir().orElse(Path.of(System.getProperty("user.home"))).resolve("healthReport_" + vault.getDisplayName() + "_" + TIME_STAMP.format(Instant.now()) + ".log");
|
||||
}
|
||||
|
||||
protected void writeReport(Collection<HealthCheckTask> tasks) throws IOException {
|
||||
protected void writeReport(Collection<Check> performedChecks) throws IOException {
|
||||
try (var out = Files.newOutputStream(exportDestination, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); //
|
||||
var writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
|
||||
writer.write(REPORT_HEADER.formatted(vaultConfig.getId(), vault.getDisplayName(), vault.getPath()));
|
||||
for (var task : tasks) {
|
||||
if (task.getState() == Worker.State.READY) {
|
||||
LOG.debug("Skipping not performed check {}.", task.getCheck().name());
|
||||
continue;
|
||||
}
|
||||
writer.write(REPORT_CHECK_HEADER.formatted(task.getCheck().name()));
|
||||
switch (task.getState()) {
|
||||
for (var check : performedChecks) {
|
||||
writer.write(REPORT_CHECK_HEADER.formatted(check.getHealthCheck().name()));
|
||||
switch (check.getState()) {
|
||||
case SUCCEEDED -> {
|
||||
writer.write("STATUS: SUCCESS\nRESULTS:\n");
|
||||
for (var result : task.results()) {
|
||||
for (var result : check.getResults()) {
|
||||
writer.write(REPORT_CHECK_RESULT.formatted(result.diagnosis().getSeverity(), result.getDescription()));
|
||||
}
|
||||
}
|
||||
case CANCELLED -> writer.write("STATUS: CANCELED\n");
|
||||
case FAILED -> {
|
||||
writer.write("STATUS: FAILED\nREASON:\n" + task.getCheck().name());
|
||||
writer.write(prepareFailureMsg(task));
|
||||
case ERROR -> {
|
||||
writer.write("STATUS: FAILED\nREASON:\n");
|
||||
writer.write(prepareFailureMsg(check));
|
||||
}
|
||||
case RUNNING, SCHEDULED -> throw new IllegalStateException("Checks are still running.");
|
||||
case RUNNABLE, RUNNING, SCHEDULED -> throw new IllegalStateException("Checks are still running.");
|
||||
case SKIPPED -> {} //noop
|
||||
}
|
||||
}
|
||||
}
|
||||
reveal();
|
||||
}
|
||||
|
||||
private String prepareFailureMsg(HealthCheckTask task) {
|
||||
if (task.getException() != null) {
|
||||
return ExceptionUtils.getStackTrace(task.getException()) //
|
||||
private String prepareFailureMsg(Check check) {
|
||||
if (check.getError() != null) {
|
||||
return Throwables.getStackTraceAsString(check.getError()) //
|
||||
.lines() //
|
||||
.map(line -> "\t\t" + line + "\n") //
|
||||
.collect(Collectors.joining());
|
||||
|
||||
@@ -17,10 +17,7 @@ record Result(DiagnosticResult diagnosis, ObjectProperty<FixState> fixState) {
|
||||
}
|
||||
|
||||
public static Result create(DiagnosticResult diagnosis) {
|
||||
FixState initialState = switch (diagnosis.getSeverity()) {
|
||||
case WARN -> FixState.FIXABLE;
|
||||
default -> FixState.NOT_FIXABLE;
|
||||
};
|
||||
FixState initialState = diagnosis.getSeverity() == DiagnosticResult.Severity.WARN ? FixState.FIXABLE : FixState.NOT_FIXABLE;
|
||||
return new Result(diagnosis, new SimpleObjectProperty<>(initialState));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import com.tobiasdiez.easybind.EasyBind;
|
||||
import com.tobiasdiez.easybind.optional.OptionalBinding;
|
||||
import com.tobiasdiez.easybind.Subscription;
|
||||
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.controls.FontAwesome5Icon;
|
||||
import org.cryptomator.ui.controls.FontAwesome5IconView;
|
||||
@@ -16,45 +17,74 @@ import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableObjectValue;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.util.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
// unscoped because each cell needs its own controller
|
||||
public class ResultListCellController implements FxController {
|
||||
|
||||
private static final FontAwesome5Icon INFO_ICON = FontAwesome5Icon.INFO_CIRCLE;
|
||||
private static final FontAwesome5Icon GOOD_ICON = FontAwesome5Icon.CHECK;
|
||||
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 ObservableObjectValue<DiagnosticResult.Severity> severity;
|
||||
private final Binding<String> description;
|
||||
private final ResultFixApplier fixApplier;
|
||||
private final OptionalBinding<Result.FixState> fixState;
|
||||
private final ObjectBinding<FontAwesome5Icon> glyph;
|
||||
private final ObservableObjectValue<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 List<Subscription> subscriptions;
|
||||
private final Tooltip fixSuccess;
|
||||
private final Tooltip fixFail;
|
||||
|
||||
public FontAwesome5IconView iconView;
|
||||
public Button fixButton;
|
||||
/* FXML */
|
||||
public FontAwesome5IconView severityView;
|
||||
public FontAwesome5IconView fixView;
|
||||
|
||||
@Inject
|
||||
public ResultListCellController(ResultFixApplier fixApplier) {
|
||||
public ResultListCellController(ResultFixApplier fixApplier, ResourceBundle resourceBundle) {
|
||||
this.result = new SimpleObjectProperty<>(null);
|
||||
this.severity = EasyBind.wrapNullable(result).map(r -> r.diagnosis().getSeverity()).asOrdinary();
|
||||
this.description = EasyBind.wrapNullable(result).map(Result::getDescription).orElse("");
|
||||
this.fixApplier = fixApplier;
|
||||
this.fixState = EasyBind.wrapNullable(result).mapObservable(Result::fixState);
|
||||
this.glyph = Bindings.createObjectBinding(this::getGlyph, result);
|
||||
this.fixState = EasyBind.wrapNullable(result).mapObservable(Result::fixState).asOrdinary();
|
||||
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.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));
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
// see getGlyph() for relevant glyphs:
|
||||
EasyBind.includeWhen(iconView.getStyleClass(), "glyph-icon-muted", iconView.glyphProperty().isEqualTo(FontAwesome5Icon.INFO_CIRCLE));
|
||||
EasyBind.includeWhen(iconView.getStyleClass(), "glyph-icon-primary", iconView.glyphProperty().isEqualTo(FontAwesome5Icon.CHECK));
|
||||
EasyBind.includeWhen(iconView.getStyleClass(), "glyph-icon-orange", iconView.glyphProperty().isEqualTo(FontAwesome5Icon.EXCLAMATION_TRIANGLE));
|
||||
EasyBind.includeWhen(iconView.getStyleClass(), "glyph-icon-red", iconView.glyphProperty().isEqualTo(FontAwesome5Icon.TIMES));
|
||||
subscriptions.addAll(List.of(EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-muted", Bindings.equal(severity, DiagnosticResult.Severity.INFO)), //
|
||||
EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-primary", Bindings.equal(severity, DiagnosticResult.Severity.GOOD)), //
|
||||
EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-orange", Bindings.equal(severity, DiagnosticResult.Severity.WARN)), //
|
||||
EasyBind.includeWhen(severityView.getStyleClass(), "glyph-icon-red", Bindings.equal(severity, DiagnosticResult.Severity.CRITICAL)) //
|
||||
// EasyBind.includeWhen(fixView.getStyleClass(), "glyph-icon-muted", fixView.glyphProperty().isNotNull())) // TODO not really needed, right?
|
||||
));
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -68,7 +98,9 @@ public class ResultListCellController implements FxController {
|
||||
private void fixFinished(Void unused, Throwable exception) {
|
||||
if (exception != null) {
|
||||
LOG.error("Failed to apply fix", exception);
|
||||
// TODO ...
|
||||
Tooltip.install(fixView, fixFail);
|
||||
} else {
|
||||
Tooltip.install(fixView, fixSuccess);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,29 +119,45 @@ public class ResultListCellController implements FxController {
|
||||
return result;
|
||||
}
|
||||
|
||||
public Binding<String> descriptionProperty() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description.getValue();
|
||||
}
|
||||
|
||||
public ObjectBinding<FontAwesome5Icon> glyphProperty() {
|
||||
return glyph;
|
||||
public ObjectBinding<FontAwesome5Icon> severityGlyphProperty() {
|
||||
return severityGlyph;
|
||||
}
|
||||
|
||||
public FontAwesome5Icon getGlyph() {
|
||||
public FontAwesome5Icon getSeverityGlyph() {
|
||||
var r = result.get();
|
||||
if (r == null) {
|
||||
return null;
|
||||
}
|
||||
return switch (r.diagnosis().getSeverity()) {
|
||||
case INFO -> FontAwesome5Icon.INFO_CIRCLE;
|
||||
case GOOD -> FontAwesome5Icon.CHECK;
|
||||
case WARN -> FontAwesome5Icon.EXCLAMATION_TRIANGLE;
|
||||
case CRITICAL -> FontAwesome5Icon.TIMES;
|
||||
case INFO -> INFO_ICON;
|
||||
case GOOD -> GOOD_ICON;
|
||||
case WARN -> WARN_ICON;
|
||||
case CRITICAL -> CRIT_ICON;
|
||||
};
|
||||
}
|
||||
|
||||
public Binding<String> descriptionProperty() {
|
||||
return description;
|
||||
public ObjectBinding<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() {
|
||||
@@ -117,7 +165,7 @@ public class ResultListCellController implements FxController {
|
||||
}
|
||||
|
||||
public boolean isFixable() {
|
||||
return fixState.get().map(Result.FixState.FIXABLE::equals).orElse(false);
|
||||
return Result.FixState.FIXABLE.equals(fixState.get());
|
||||
}
|
||||
|
||||
public BooleanBinding fixingProperty() {
|
||||
@@ -125,7 +173,7 @@ public class ResultListCellController implements FxController {
|
||||
}
|
||||
|
||||
public boolean isFixing() {
|
||||
return fixState.get().map(Result.FixState.FIXING::equals).orElse(false);
|
||||
return Result.FixState.FIXING.equals(fixState.get());
|
||||
}
|
||||
|
||||
public BooleanBinding fixedProperty() {
|
||||
@@ -133,7 +181,24 @@ public class ResultListCellController implements FxController {
|
||||
}
|
||||
|
||||
public boolean isFixed() {
|
||||
return fixState.get().map(Result.FixState.FIXED::equals).orElse(false);
|
||||
return Result.FixState.FIXED.equals(fixState.get());
|
||||
}
|
||||
|
||||
public BooleanBinding fixFailedProperty() {
|
||||
return fixFailed;
|
||||
}
|
||||
|
||||
public Boolean isFixFailed() {
|
||||
return Result.FixState.FIX_FAILED.equals(fixState.get());
|
||||
}
|
||||
|
||||
public BooleanBinding fixRunningOrDoneProperty() {
|
||||
return fixRunningOrDone;
|
||||
}
|
||||
|
||||
public boolean isFixRunningOrDone() {
|
||||
return fixRunningOrDone.get();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.cryptofs.VaultConfig;
|
||||
import org.cryptomator.cryptofs.VaultConfigLoadException;
|
||||
import org.cryptomator.cryptofs.VaultKeyInvalidException;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
@@ -17,15 +16,13 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
@@ -36,40 +33,28 @@ public class StartController implements FxController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(StartController.class);
|
||||
|
||||
private final Vault vault;
|
||||
private final Stage window;
|
||||
private final CompletableFuture<VaultConfig.UnverifiedVaultConfig> unverifiedVaultConfig;
|
||||
private final Stage unlockWindow;
|
||||
private final ObjectProperty<VaultConfig.UnverifiedVaultConfig> unverifiedVaultConfig;
|
||||
private final KeyLoadingStrategy keyLoadingStrategy;
|
||||
private final ExecutorService executor;
|
||||
private final AtomicReference<Masterkey> masterkeyRef;
|
||||
private final AtomicReference<VaultConfig> vaultConfigRef;
|
||||
private final Lazy<Scene> checkScene;
|
||||
private final Lazy<ErrorComponent.Builder> errorComponent;
|
||||
private final ObjectProperty<State> state = new SimpleObjectProperty<>(State.LOADING);
|
||||
private final BooleanBinding loading = state.isEqualTo(State.LOADING);
|
||||
private final BooleanBinding failed = state.isEqualTo(State.FAILED);
|
||||
private final BooleanBinding loaded = state.isEqualTo(State.LOADED);
|
||||
|
||||
public enum State {
|
||||
LOADING,
|
||||
FAILED,
|
||||
LOADED
|
||||
}
|
||||
|
||||
/* FXML */
|
||||
|
||||
@Inject
|
||||
public StartController(@HealthCheckWindow Vault vault, @HealthCheckWindow Stage window, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy<Scene> checkScene, Lazy<ErrorComponent.Builder> errorComponent) {
|
||||
this.vault = vault;
|
||||
public StartController(@HealthCheckWindow Stage window, HealthCheckComponent.LoadUnverifiedConfigResult configLoadResult, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy<Scene> checkScene, Lazy<ErrorComponent.Builder> errorComponent, @Named("unlockWindow") Stage unlockWindow) {
|
||||
Preconditions.checkNotNull(configLoadResult.config());
|
||||
this.window = window;
|
||||
this.unverifiedVaultConfig = CompletableFuture.supplyAsync(this::loadConfig, executor);
|
||||
this.unlockWindow = unlockWindow;
|
||||
this.unverifiedVaultConfig = new SimpleObjectProperty<>(configLoadResult.config());
|
||||
this.keyLoadingStrategy = keyLoadingStrategy;
|
||||
this.executor = executor;
|
||||
this.masterkeyRef = masterkeyRef;
|
||||
this.vaultConfigRef = vaultConfigRef;
|
||||
this.checkScene = checkScene;
|
||||
this.errorComponent = errorComponent;
|
||||
this.unverifiedVaultConfig.whenCompleteAsync(this::loadedConfig, Platform::runLater);
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -84,29 +69,18 @@ public class StartController implements FxController {
|
||||
CompletableFuture.runAsync(this::loadKey, executor).whenCompleteAsync(this::loadedKey, Platform::runLater);
|
||||
}
|
||||
|
||||
private VaultConfig.UnverifiedVaultConfig loadConfig() {
|
||||
assert !Platform.isFxApplicationThread();
|
||||
try {
|
||||
return this.vault.getUnverifiedVaultConfig();
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadedConfig(VaultConfig.UnverifiedVaultConfig cfg, Throwable exception) {
|
||||
assert Platform.isFxApplicationThread();
|
||||
if (exception != null) {
|
||||
state.set(State.FAILED);
|
||||
} else {
|
||||
assert cfg != null;
|
||||
state.set(State.LOADED);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadKey() {
|
||||
assert !Platform.isFxApplicationThread();
|
||||
assert unverifiedVaultConfig.isDone();
|
||||
var unverifiedCfg = unverifiedVaultConfig.join();
|
||||
assert unverifiedVaultConfig.get() != null;
|
||||
try {
|
||||
keyLoadingStrategy.use(this::verifyVaultConfig);
|
||||
} catch (VaultConfigLoadException | UnlockCancelledException e) {
|
||||
throw new LoadingFailedException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyVaultConfig(KeyLoadingStrategy keyLoadingStrategy) throws VaultConfigLoadException {
|
||||
var unverifiedCfg = unverifiedVaultConfig.get();
|
||||
try (var masterkey = keyLoadingStrategy.loadKey(unverifiedCfg.getKeyId())) {
|
||||
var verifiedCfg = unverifiedCfg.verify(masterkey.getEncoded(), unverifiedCfg.allegedVaultVersion());
|
||||
vaultConfigRef.set(verifiedCfg);
|
||||
@@ -114,15 +88,6 @@ public class StartController implements FxController {
|
||||
if (old != null) {
|
||||
old.destroy();
|
||||
}
|
||||
} catch (MasterkeyLoadingFailedException e) {
|
||||
if (keyLoadingStrategy.recoverFromException(e)) {
|
||||
// retry
|
||||
loadKey();
|
||||
} else {
|
||||
throw new LoadingFailedException(e);
|
||||
}
|
||||
} catch (VaultConfigLoadException e) {
|
||||
throw new LoadingFailedException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +99,7 @@ public class StartController implements FxController {
|
||||
loadingKeyFailed(exception);
|
||||
} else {
|
||||
LOG.debug("Loaded valid key");
|
||||
unlockWindow.close();
|
||||
window.setScene(checkScene.get());
|
||||
}
|
||||
}
|
||||
@@ -150,35 +116,10 @@ public class StartController implements FxController {
|
||||
}
|
||||
}
|
||||
|
||||
/* Getter */
|
||||
|
||||
public BooleanBinding loadingProperty() {
|
||||
return loading;
|
||||
}
|
||||
|
||||
public boolean isLoading() {
|
||||
return loading.get();
|
||||
}
|
||||
|
||||
public BooleanBinding failedProperty() {
|
||||
return failed;
|
||||
}
|
||||
|
||||
public boolean isFailed() {
|
||||
return failed.get();
|
||||
}
|
||||
|
||||
public BooleanBinding loadedProperty() {
|
||||
return loaded;
|
||||
}
|
||||
|
||||
public boolean isLoaded() {
|
||||
return loaded.get();
|
||||
}
|
||||
|
||||
/* internal types */
|
||||
|
||||
private static class LoadingFailedException extends CompletionException {
|
||||
|
||||
LoadingFailedException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package org.cryptomator.ui.health;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.cryptomator.cryptofs.VaultConfigLoadException;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.controls.FontAwesome5Icon;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.TitledPane;
|
||||
import javafx.stage.Stage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
// TODO reevaluate config loading, as soon as we have the new generic error screen
|
||||
@HealthCheckScoped
|
||||
public class StartFailController implements FxController {
|
||||
|
||||
private final Stage window;
|
||||
private final ObjectProperty<Throwable> loadError;
|
||||
private final ObjectProperty<FontAwesome5Icon> moreInfoIcon;
|
||||
|
||||
/* FXML */
|
||||
public TitledPane moreInfoPane;
|
||||
|
||||
@Inject
|
||||
public StartFailController(@HealthCheckWindow Stage window, HealthCheckComponent.LoadUnverifiedConfigResult configLoadResult) {
|
||||
Preconditions.checkNotNull(configLoadResult.error());
|
||||
this.window = window;
|
||||
this.loadError = new SimpleObjectProperty<>(configLoadResult.error());
|
||||
this.moreInfoIcon = new SimpleObjectProperty<>(FontAwesome5Icon.CARET_RIGHT);
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
moreInfoPane.expandedProperty().addListener(this::setMoreInfoIcon);
|
||||
}
|
||||
|
||||
private void setMoreInfoIcon(ObservableValue<? extends Boolean> observable, boolean wasExpanded, boolean willExpand) {
|
||||
moreInfoIcon.set(willExpand ? FontAwesome5Icon.CARET_DOWN : FontAwesome5Icon.CARET_RIGHT);
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void close() {
|
||||
window.close();
|
||||
}
|
||||
|
||||
/* Getter & Setter */
|
||||
|
||||
public ObjectProperty<FontAwesome5Icon> moreInfoIconProperty() {
|
||||
return moreInfoIcon;
|
||||
}
|
||||
|
||||
public FontAwesome5Icon getMoreInfoIcon() {
|
||||
return moreInfoIcon.getValue();
|
||||
}
|
||||
|
||||
public String getStackTrace() {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
loadError.get().printStackTrace(new PrintStream(baos));
|
||||
return baos.toString(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public String getLocalizedErrorMessage() {
|
||||
return loadError.get().getLocalizedMessage();
|
||||
}
|
||||
|
||||
public boolean isParseException() {
|
||||
return loadError.get() instanceof VaultConfigLoadException;
|
||||
}
|
||||
|
||||
public boolean isIoException() {
|
||||
return !isParseException();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package org.cryptomator.ui.keyloading;
|
||||
import org.cryptomator.cryptolib.api.Masterkey;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoader;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
@@ -12,6 +14,8 @@ import java.net.URI;
|
||||
@FunctionalInterface
|
||||
public interface KeyLoadingStrategy extends MasterkeyLoader {
|
||||
|
||||
Logger LOG = LoggerFactory.getLogger(KeyLoadingStrategy.class);
|
||||
|
||||
/**
|
||||
* Loads a master key. This might be a long-running operation, as it may require user input or expensive computations.
|
||||
* <p>
|
||||
@@ -60,4 +64,36 @@ public interface KeyLoadingStrategy extends MasterkeyLoader {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the given <code>user</code> apply this key loading strategy. If the user fails with a {@link MasterkeyLoadingFailedException},
|
||||
* an attempt is made to {@link #recoverFromException(MasterkeyLoadingFailedException) recover} from it. Any other exception will be rethrown.
|
||||
*
|
||||
* @param user Some method using this strategy. May be invoked multiple times in case of recoverable {@link MasterkeyLoadingFailedException}s
|
||||
* @param <E> Optional exception type thrown by <code>user</code>
|
||||
* @throws MasterkeyLoadingFailedException If a non-recoverable exception is thrown by <code>user</code>
|
||||
* @throws E Exception thrown by <code>user</code> and rethrown by this method
|
||||
*/
|
||||
default <E extends Exception> void use(KeyLoadingStrategyUser<E> user) throws MasterkeyLoadingFailedException, E {
|
||||
boolean success = false;
|
||||
try {
|
||||
user.use(this);
|
||||
} catch (MasterkeyLoadingFailedException e) {
|
||||
if (recoverFromException(e)) {
|
||||
LOG.info("Unlock attempt threw {}. Reattempting...", e.getClass().getSimpleName());
|
||||
use(user);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
cleanup(success);
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface KeyLoadingStrategyUser<E extends Exception> {
|
||||
|
||||
void use(KeyLoadingStrategy strategy) throws MasterkeyLoadingFailedException, E;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.cryptomator.ui.unlock;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import dagger.Lazy;
|
||||
import org.cryptomator.common.mountpoint.InvalidMountPointException;
|
||||
import org.cryptomator.common.vaults.MountPointRequirement;
|
||||
@@ -7,12 +8,10 @@ import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.common.vaults.VaultState;
|
||||
import org.cryptomator.common.vaults.Volume.VolumeException;
|
||||
import org.cryptomator.cryptolib.api.CryptoException;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
import org.cryptomator.ui.common.ErrorComponent;
|
||||
import org.cryptomator.ui.common.FxmlFile;
|
||||
import org.cryptomator.ui.common.FxmlScene;
|
||||
import org.cryptomator.ui.common.VaultService;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingComponent;
|
||||
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -68,19 +67,14 @@ public class UnlockWorkflow extends Task<Boolean> {
|
||||
}
|
||||
|
||||
private void attemptUnlock() throws IOException, VolumeException, InvalidMountPointException, CryptoException {
|
||||
boolean success = false;
|
||||
try {
|
||||
vault.unlock(keyLoadingStrategy);
|
||||
success = true;
|
||||
} catch (MasterkeyLoadingFailedException e) {
|
||||
if (keyLoadingStrategy.recoverFromException(e)) {
|
||||
LOG.info("Unlock attempt threw {}. Reattempting...", e.getClass().getSimpleName());
|
||||
attemptUnlock();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
keyLoadingStrategy.cleanup(success);
|
||||
keyLoadingStrategy.use(vault::unlock);
|
||||
} catch (Exception e) {
|
||||
Throwables.propagateIfPossible(e, IOException.class);
|
||||
Throwables.propagateIfPossible(e, VolumeException.class);
|
||||
Throwables.propagateIfPossible(e, InvalidMountPointException.class);
|
||||
Throwables.propagateIfPossible(e, CryptoException.class);
|
||||
throw new IllegalStateException("unexpected exception type", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ import org.cryptomator.common.settings.WhenUnlocked;
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.controls.NumericTextField;
|
||||
import org.cryptomator.ui.health.HealthCheckComponent;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.ChoiceBox;
|
||||
@@ -24,6 +26,7 @@ public class GeneralVaultOptionsController implements FxController {
|
||||
|
||||
private final Stage window;
|
||||
private final Vault vault;
|
||||
private final HealthCheckComponent.Builder healthCheckWindow;
|
||||
private final ResourceBundle resourceBundle;
|
||||
|
||||
public TextField vaultName;
|
||||
@@ -33,9 +36,10 @@ public class GeneralVaultOptionsController implements FxController {
|
||||
public NumericTextField lockTimeInMinutesTextField;
|
||||
|
||||
@Inject
|
||||
GeneralVaultOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, ResourceBundle resourceBundle) {
|
||||
GeneralVaultOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, HealthCheckComponent.Builder healthCheckWindow, ResourceBundle resourceBundle) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.healthCheckWindow = healthCheckWindow;
|
||||
this.resourceBundle = resourceBundle;
|
||||
}
|
||||
|
||||
@@ -104,4 +108,8 @@ public class GeneralVaultOptionsController implements FxController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void startHealthCheck() {
|
||||
healthCheckWindow.vault(vault).owner(window).build().showHealthCheckWindow();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package org.cryptomator.ui.vaultoptions;
|
||||
|
||||
import org.cryptomator.common.vaults.Vault;
|
||||
import org.cryptomator.ui.common.FxController;
|
||||
import org.cryptomator.ui.health.HealthCheckComponent;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
@VaultOptionsScoped
|
||||
public class HealthVaultOptionsController implements FxController {
|
||||
|
||||
private final Stage window;
|
||||
private final Vault vault;
|
||||
private final HealthCheckComponent.Builder healthCheckWindow;
|
||||
|
||||
@Inject
|
||||
public HealthVaultOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, HealthCheckComponent.Builder healthCheckWindow) {
|
||||
this.window = window;
|
||||
this.vault = vault;
|
||||
this.healthCheckWindow = healthCheckWindow;
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void startHealthCheck(ActionEvent event) {
|
||||
healthCheckWindow.vault(vault).windowToClose(window).build().showHealthCheckWindow();
|
||||
}
|
||||
}
|
||||
@@ -25,9 +25,4 @@ public enum SelectedVaultOptionsTab {
|
||||
* Show Auto-Lock tab
|
||||
*/
|
||||
AUTOLOCK,
|
||||
|
||||
/**
|
||||
* Show health tab
|
||||
*/
|
||||
HEALTH;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ public class VaultOptionsController implements FxController {
|
||||
public Tab mountTab;
|
||||
public Tab keyTab;
|
||||
public Tab autoLockTab;
|
||||
public Tab healthTab;
|
||||
|
||||
@Inject
|
||||
VaultOptionsController(@VaultOptionsWindow Stage window, ObjectProperty<SelectedVaultOptionsTab> selectedTabProperty) {
|
||||
@@ -50,7 +49,6 @@ public class VaultOptionsController implements FxController {
|
||||
case MOUNT -> mountTab;
|
||||
case KEY -> keyTab;
|
||||
case AUTOLOCK -> autoLockTab;
|
||||
case HEALTH -> healthTab;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -83,9 +83,4 @@ abstract class VaultOptionsModule {
|
||||
@IntoMap
|
||||
@FxControllerKey(MasterkeyOptionsController.class)
|
||||
abstract FxController bindMasterkeyOptionsController(MasterkeyOptionsController controller);
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FxControllerKey(HealthVaultOptionsController.class)
|
||||
abstract FxController bindHealthOptionsController(HealthVaultOptionsController controller);
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
org.cryptomator.ui.health.DummyHealthChecks$DummyCheck1
|
||||
org.cryptomator.ui.health.DummyHealthChecks$DummyCheck2
|
||||
org.cryptomator.ui.health.DummyHealthChecks$DummyCheck3
|
||||
@@ -127,23 +127,38 @@
|
||||
-fx-fill: TEXT_FILL;
|
||||
}
|
||||
|
||||
.glyph-icon-primary {
|
||||
.glyph-icon-primary,
|
||||
.glyph-icon.glyph-icon-primary,
|
||||
.list-cell .glyph-icon.glyph-icon-primary,
|
||||
.list-cell:selected .glyph-icon.glyph-icon-primary {
|
||||
-fx-fill: PRIMARY;
|
||||
}
|
||||
|
||||
.glyph-icon-muted {
|
||||
.glyph-icon-muted,
|
||||
.glyph-icon.glyph-icon-muted,
|
||||
.list-cell .glyph-icon.glyph-icon-muted,
|
||||
.list-cell:selected .glyph-icon.glyph-icon-muted {
|
||||
-fx-fill: TEXT_FILL_MUTED;
|
||||
}
|
||||
|
||||
.glyph-icon-white {
|
||||
.glyph-icon-white,
|
||||
.glyph-icon.glyph-icon-white,
|
||||
.list-cell .glyph-icon.glyph-icon-white,
|
||||
.list-cell:selected .glyph-icon.glyph-icon-white {
|
||||
-fx-fill: white;
|
||||
}
|
||||
|
||||
.glyph-icon-red {
|
||||
.glyph-icon-red,
|
||||
.glyph-icon.glyph-icon-red,
|
||||
.list-cell .glyph-icon.glyph-icon-red,
|
||||
.list-cell:selected .glyph-icon.glyph-icon-red {
|
||||
-fx-fill: RED_5;
|
||||
}
|
||||
|
||||
.glyph-icon-orange {
|
||||
.glyph-icon-orange,
|
||||
.glyph-icon.glyph-icon-orange,
|
||||
.list-cell .glyph-icon.glyph-icon-orange,
|
||||
.list-cell:selected .glyph-icon.glyph-icon-orange {
|
||||
-fx-fill: ORANGE_5;
|
||||
}
|
||||
|
||||
|
||||
@@ -127,23 +127,38 @@
|
||||
-fx-fill: TEXT_FILL;
|
||||
}
|
||||
|
||||
.glyph-icon-primary {
|
||||
.glyph-icon-primary,
|
||||
.glyph-icon.glyph-icon-primary,
|
||||
.list-cell .glyph-icon.glyph-icon-primary,
|
||||
.list-cell:selected .glyph-icon.glyph-icon-primary {
|
||||
-fx-fill: PRIMARY;
|
||||
}
|
||||
|
||||
.glyph-icon-muted {
|
||||
.glyph-icon-muted,
|
||||
.glyph-icon.glyph-icon-muted,
|
||||
.list-cell .glyph-icon.glyph-icon-muted,
|
||||
.list-cell:selected .glyph-icon.glyph-icon-muted {
|
||||
-fx-fill: TEXT_FILL_MUTED;
|
||||
}
|
||||
|
||||
.glyph-icon-white {
|
||||
.glyph-icon-white,
|
||||
.glyph-icon.glyph-icon-white,
|
||||
.list-cell .glyph-icon.glyph-icon-white,
|
||||
.list-cell:selected .glyph-icon.glyph-icon-white {
|
||||
-fx-fill: white;
|
||||
}
|
||||
|
||||
.glyph-icon-red {
|
||||
.glyph-icon-red,
|
||||
.glyph-icon.glyph-icon-red,
|
||||
.list-cell .glyph-icon.glyph-icon-red,
|
||||
.list-cell:selected .glyph-icon.glyph-icon-red {
|
||||
-fx-fill: RED_5;
|
||||
}
|
||||
|
||||
.glyph-icon-orange {
|
||||
.glyph-icon-orange,
|
||||
.glyph-icon.glyph-icon-orange,
|
||||
.list-cell .glyph-icon.glyph-icon-orange,
|
||||
.list-cell:selected .glyph-icon.glyph-icon-orange {
|
||||
-fx-fill: ORANGE_5;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import org.cryptomator.ui.controls.FormattedLabel?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ListView?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import org.cryptomator.ui.health.CheckStateIconView?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.health.CheckDetailController"
|
||||
prefWidth="500"
|
||||
spacing="6">
|
||||
<FormattedLabel fx:id="checkTitle" styleClass="label-large" format="%health.check.detail.header" arg1="${controller.taskName}"/>
|
||||
<Label fx:id="detailHeader" styleClass="label-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>
|
||||
|
||||
<Label text="%health.check.detail.taskNotStarted" visible="${controller.taskNotStarted}" managed="${controller.taskNotStarted}"/>
|
||||
<Label text="%health.check.detail.taskRunning" visible="${controller.taskRunning}" managed="${controller.taskRunning}"/>
|
||||
<Label text="%health.check.detail.taskScheduled" visible="${controller.taskScheduled}" managed="${controller.taskScheduled}"/>
|
||||
<Label text="%health.check.detail.taskCancelled" visible="${controller.taskCancelled}" managed="${controller.taskCancelled}"/>
|
||||
<Label text="%health.check.detail.taskFailed" visible="${controller.taskFailed}" managed="${controller.taskFailed}"/>
|
||||
<FormattedLabel styleClass="label" format="%health.check.detail.taskSucceeded" arg1="${controller.taskDuration}" visible="${controller.taskSucceeded}" managed="${controller.taskSucceeded}"/>
|
||||
<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 && !controller.warnOrCritsExist}" managed="${controller.checkSucceeded && !controller.warnOrCritsExist}"/>
|
||||
<Label text="%health.check.detail.checkFinishedAndFound" visible="${controller.checkSucceeded && controller.warnOrCritsExist}" managed="${controller.checkSucceeded && controller.warnOrCritsExist}"/>
|
||||
|
||||
<FormattedLabel styleClass="label" format="%health.check.detail.problemCount" arg1="${controller.countOfWarnSeverity}" arg2="${controller.countOfCritSeverity}" visible="${!controller.taskNotStarted}"
|
||||
managed="${!controller.taskNotStarted}" />
|
||||
<ListView fx:id="resultsListView" VBox.vgrow="ALWAYS"/>
|
||||
<ListView fx:id="resultsListView" VBox.vgrow="ALWAYS" visible="${!controller.checkSkipped}" fixedCellSize="25"/>
|
||||
</VBox>
|
||||
@@ -24,23 +24,26 @@
|
||||
</fx:define>
|
||||
<children>
|
||||
<HBox spacing="12" VBox.vgrow="ALWAYS">
|
||||
<VBox minWidth="80" maxWidth="200" spacing="6" HBox.hgrow="ALWAYS" >
|
||||
<Label fx:id="listHeading" text="%health.checkList.header"/>
|
||||
<CheckBox onAction="#toggleSelectAll" text="%health.checkList.selectAllBox" visible="${!controller.showResultScreen}" managed="${!controller.showResultScreen}" />
|
||||
<ListView fx:id="checksListView" VBox.vgrow="ALWAYS"/>
|
||||
<ListView fx:id="checksListView" VBox.vgrow="ALWAYS" minWidth="175" maxWidth="175"/>
|
||||
<VBox alignment="CENTER" visible="${!controller.mainRunStarted}" managed="${!controller.mainRunStarted}" HBox.hgrow="ALWAYS" spacing="12">
|
||||
<Label text="%health.checkList.description" wrapText="true"/>
|
||||
<HBox alignment="CENTER">
|
||||
<Button onAction="#selectAllChecks" text="%health.checkList.selectAllButton" />
|
||||
<Button onAction="#deselectAllChecks" text="%health.checkList.deselectAllButton" />
|
||||
</HBox>
|
||||
</VBox>
|
||||
<StackPane visible="${controller.showResultScreen}" HBox.hgrow="ALWAYS" >
|
||||
<StackPane visible="${controller.mainRunStarted}" managed="${controller.mainRunStarted}" HBox.hgrow="ALWAYS">
|
||||
<VBox minWidth="300" alignment="CENTER" visible="${!controller.anyCheckSelected}" managed="${!controller.anyCheckSelected}" >
|
||||
<Label text="%health.check.detail.noSelectedCheck" wrapText="true" alignment="CENTER" />
|
||||
</VBox>
|
||||
<fx:include source="health_check_details.fxml" visible="${controller.anyCheckSelected}" managed="${controller.anyCheckSelected}" />
|
||||
<fx:include source="health_check_details.fxml" visible="${controller.anyCheckSelected}" managed="${controller.anyCheckSelected}" HBox.hgrow="ALWAYS"/>
|
||||
</StackPane>
|
||||
</HBox>
|
||||
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
|
||||
<buttons>
|
||||
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" onAction="#cancelCheck" disable="${!controller.running}" visible="${controller.showResultScreen}" managed="${controller.showResultScreen}" />
|
||||
<Button text="%health.check.exportBtn" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" disable="${!controller.finished}" visible="${controller.showResultScreen}" managed="${controller.showResultScreen}" onAction="#exportResults"/>
|
||||
<Button text="%health.check.runBatchBtn" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#runSelectedChecks" disable="${controller.chosenTaskCount == ZERO}" visible="${!controller.showResultScreen}" managed="${!controller.showResultScreen}"/>
|
||||
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" onAction="#cancelRun" visible="${controller.running}" managed="${controller.running}" />
|
||||
<Button text="%health.check.exportBtn" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" disable="${controller.running}" visible="${controller.mainRunStarted}" managed="${controller.mainRunStarted}" onAction="#exportResults"/>
|
||||
<Button text="%health.check.runBatchBtn" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#runSelectedChecks" disable="${controller.chosenTaskCount == ZERO}" visible="${!controller.mainRunStarted}" managed="${!controller.mainRunStarted}"/>
|
||||
</buttons>
|
||||
</ButtonBar>
|
||||
</children>
|
||||
|
||||
26
src/main/resources/fxml/health_check_listcell.fxml
Normal file
26
src/main/resources/fxml/health_check_listcell.fxml
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
|
||||
<?import org.cryptomator.ui.health.CheckStateIconView?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<?import javafx.scene.control.CheckBox?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
|
||||
<HBox xmlns="http://javafx.com/javafx"
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
fx:controller="org.cryptomator.ui.health.CheckListCellController"
|
||||
prefHeight="30.0" prefWidth="150.0"
|
||||
alignment="CENTER_LEFT" spacing="6">
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="6"/>
|
||||
</padding>
|
||||
|
||||
<StackPane minWidth="20" minHeight="20" alignment="CENTER">
|
||||
<CheckBox fx:id="forRunSelectedCheckBox" visible="${controller.checkRunnable}" />
|
||||
<CheckStateIconView check="${controller.check}" glyphSize="20" visible="${!controller.checkRunnable}"/>
|
||||
</StackPane>
|
||||
<Label text="${controller.checkName}"/>
|
||||
|
||||
</HBox>
|
||||
@@ -6,9 +6,6 @@
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.Region?>
|
||||
<?import javafx.scene.control.ProgressIndicator?>
|
||||
<?import javafx.scene.text.Text?>
|
||||
<?import javafx.scene.layout.Pane?>
|
||||
<?import javafx.scene.layout.StackPane?>
|
||||
<HBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
@@ -22,18 +19,14 @@
|
||||
<Insets topRightBottomLeft="6"/>
|
||||
</padding>
|
||||
<children>
|
||||
<FontAwesome5IconView fx:id="iconView" HBox.hgrow="NEVER" glyphSize="16" glyph="${controller.glyph}"/>
|
||||
<StackPane minWidth="25" minHeight="25">
|
||||
<FontAwesome5IconView fx:id="severityView" HBox.hgrow="NEVER" glyphSize="16" glyph="${controller.severityGlyph}"/>
|
||||
</StackPane>
|
||||
<Label text="${controller.description}"/>
|
||||
<Region HBox.hgrow="ALWAYS"/>
|
||||
<!-- TODO: setting the minWidth of the button is just a workaround.
|
||||
What we actually want to do is to prevent shrinking the button more than the text
|
||||
-> own subclass of HBox is needed -->
|
||||
<StackPane HBox.hgrow="NEVER">
|
||||
<children>
|
||||
<Button fx:id="fixButton" text="%health.check.fixBtn" visible="${controller.fixable}" managed="${controller.fixable}" onAction="#fix" alignment="CENTER" minWidth="-Infinity"/>
|
||||
<ProgressIndicator progress="-1" prefWidth="12" prefHeight="12" visible="${controller.fixing}" managed="${controller.fixing}"/>
|
||||
<FontAwesome5IconView glyph="CHECK" glyphSize="16" visible="${controller.fixed}" managed="${controller.fixed}"/>
|
||||
</children>
|
||||
<Button fx:id="fixButton" text="%health.fix.fixBtn" visible="${controller.fixable}" onAction="#fix" alignment="CENTER" minWidth="-Infinity"/>
|
||||
<FontAwesome5IconView fx:id="fixView" styleClass="glyph-icon-muted" glyph="${controller.fixGlyph}" glyphSize="16" visible="${controller.fixRunningOrDone}" managed="${controller.fixRunningOrDone}"/>
|
||||
</StackPane>
|
||||
</children>
|
||||
</HBox>
|
||||
|
||||
@@ -1,41 +1,71 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.ButtonBar?>
|
||||
<?import javafx.scene.control.CheckBox?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.image.Image?>
|
||||
<?import javafx.scene.image.ImageView?>
|
||||
<?import javafx.scene.layout.ColumnConstraints?>
|
||||
<?import javafx.scene.layout.GridPane?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.Region?>
|
||||
<?import javafx.scene.layout.RowConstraints?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.health.StartController"
|
||||
minWidth="400"
|
||||
maxWidth="400"
|
||||
minHeight="145"
|
||||
prefWidth="600"
|
||||
prefHeight="400"
|
||||
spacing="12">
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="12"/>
|
||||
</padding>
|
||||
<children>
|
||||
<Label text="TODO loading config..." visible="${controller.loading}" managed="${controller.loading}" wrapText="true" contentDisplay="LEFT">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="SPINNER"/>
|
||||
</graphic>
|
||||
</Label>
|
||||
<Label text="%health.start.configInvalid" visible="${controller.failed}" managed="${controller.failed}" wrapText="true" contentDisplay="LEFT">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="TIMES" styleClass="glyph-icon-red"/>
|
||||
</graphic>
|
||||
</Label>
|
||||
<Label text="%health.start.configValid" visible="${controller.loaded}" managed="${controller.loaded}" wrapText="true" contentDisplay="LEFT">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="CHECK" styleClass="glyph-icon-primary"/>
|
||||
</graphic>
|
||||
</Label>
|
||||
<HBox VBox.vgrow="ALWAYS">
|
||||
<VBox alignment="CENTER" minWidth="175" maxWidth="175">
|
||||
<ImageView VBox.vgrow="ALWAYS" fitHeight="128" preserveRatio="true" smooth="true" cache="true">
|
||||
<Image url="@../img/bot/bot.png"/>
|
||||
</ImageView>
|
||||
</VBox>
|
||||
<VBox HBox.hgrow="ALWAYS" alignment="CENTER">
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="12"/>
|
||||
</padding>
|
||||
<Label text="%health.intro.header" styleClass="label-large"/>
|
||||
<Region minHeight="15"/>
|
||||
<VBox>
|
||||
<Label text="%health.intro.text" wrapText="true"/>
|
||||
<GridPane alignment="CENTER_LEFT" >
|
||||
<padding>
|
||||
<Insets left="6"/>
|
||||
</padding>
|
||||
<columnConstraints>
|
||||
<ColumnConstraints minWidth="20" halignment="LEFT"/>
|
||||
<ColumnConstraints fillWidth="true"/>
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints valignment="TOP"/>
|
||||
<RowConstraints valignment="TOP"/>
|
||||
<RowConstraints valignment="TOP"/>
|
||||
</rowConstraints>
|
||||
<Label text="1." GridPane.rowIndex="0" GridPane.columnIndex="0" />
|
||||
<Label text="%health.intro.remarkSync" wrapText="true" GridPane.rowIndex="0" GridPane.columnIndex="1" />
|
||||
<Label text="2." GridPane.rowIndex="1" GridPane.columnIndex="0" />
|
||||
<Label text="%health.intro.remarkFix" wrapText="true" GridPane.rowIndex="1" GridPane.columnIndex="1" />
|
||||
<Label text="3." GridPane.rowIndex="2" GridPane.columnIndex="0" />
|
||||
<Label text="%health.intro.remarkBackup" wrapText="true" GridPane.rowIndex="2" GridPane.columnIndex="1" />
|
||||
</GridPane>
|
||||
<Region minHeight="15"/>
|
||||
<CheckBox text="%health.intro.affirmation" fx:id="affirmationBox"/>
|
||||
</VBox>
|
||||
</VBox>
|
||||
</HBox>
|
||||
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
|
||||
<buttons>
|
||||
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
|
||||
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" disable="${!controller.loaded}" defaultButton="true" onAction="#next"/>
|
||||
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" disable="${!affirmationBox.selected}" defaultButton="true" onAction="#next"/>
|
||||
</buttons>
|
||||
</ButtonBar>
|
||||
</children>
|
||||
|
||||
44
src/main/resources/fxml/health_start_fail.fxml
Normal file
44
src/main/resources/fxml/health_start_fail.fxml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ButtonBar?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.TextArea?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.TitledPane?>
|
||||
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
|
||||
<?import javafx.scene.layout.Region?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.text.TextFlow?>
|
||||
<?import javafx.scene.text.Text?>
|
||||
<VBox xmlns="http://javafx.com/javafx"
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
fx:controller="org.cryptomator.ui.health.StartFailController"
|
||||
prefWidth="600"
|
||||
prefHeight="400"
|
||||
spacing="12">
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="12"/>
|
||||
</padding>
|
||||
<Label text="%health.fail.header" styleClass="label-large" />
|
||||
<TextFlow fx:id="ioErrorLabel" visible="${controller.ioException}" managed="${controller.ioException}">
|
||||
<Text text="%health.fail.ioError" />
|
||||
<Text text="${controller.localizedErrorMessage}"/>
|
||||
</TextFlow>
|
||||
<Label fx:id="parseErrorLabel" text="%health.fail.parseError" visible="${controller.parseException}" managed="${controller.parseException}"/>
|
||||
<TitledPane fx:id="moreInfoPane" text="%health.fail.moreInfo" expanded="false">
|
||||
<graphic>
|
||||
<HBox alignment="CENTER" minWidth="8">
|
||||
<FontAwesome5IconView glyph="${controller.moreInfoIcon}"/>
|
||||
</HBox>
|
||||
</graphic>
|
||||
<content>
|
||||
<TextArea VBox.vgrow="ALWAYS" text="${controller.stackTrace}" prefRowCount="20" editable="false" />
|
||||
</content>
|
||||
</TitledPane>
|
||||
<Region VBox.vgrow="ALWAYS"/>
|
||||
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
|
||||
<buttons>
|
||||
<Button text="%generic.button.close" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
|
||||
</buttons>
|
||||
</ButtonBar>
|
||||
</VBox>
|
||||
@@ -36,13 +36,5 @@
|
||||
<fx:include source="vault_options_masterkey.fxml"/>
|
||||
</content>
|
||||
</Tab>
|
||||
<Tab fx:id="healthTab" id="HEALTH" text="%vaultOptions.health">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="STETHOSCOPE"/>
|
||||
</graphic>
|
||||
<content>
|
||||
<fx:include source="vault_options_health.fxml"/>
|
||||
</content>
|
||||
</Tab>
|
||||
</tabs>
|
||||
</TabPane>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<?import javafx.scene.text.Text?>
|
||||
<?import org.cryptomator.ui.controls.NumericTextField?>
|
||||
<?import org.cryptomator.ui.controls.FormattedLabel?>
|
||||
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
|
||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.vaultoptions.GeneralVaultOptionsController"
|
||||
@@ -39,5 +40,11 @@
|
||||
<Label text="%vaultOptions.general.actionAfterUnlock"/>
|
||||
<ChoiceBox fx:id="actionAfterUnlockChoiceBox"/>
|
||||
</HBox>
|
||||
|
||||
<Button fx:id="healthCheckButton" text="%vaultOptions.general.startHealthCheckBtn" onAction="#startHealthCheck">
|
||||
<graphic>
|
||||
<FontAwesome5IconView glyph="STETHOSCOPE"/>
|
||||
</graphic>
|
||||
</Button>
|
||||
</children>
|
||||
</VBox>
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.shape.Box?>
|
||||
<?import javafx.scene.Group?>
|
||||
<?import javafx.scene.layout.GridPane?>
|
||||
<?import javafx.scene.layout.ColumnConstraints?>
|
||||
<?import javafx.collections.ObservableList?>
|
||||
<?import javafx.collections.FXCollections?>
|
||||
<?import javafx.scene.layout.RowConstraints?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.control.CheckBox?>
|
||||
<VBox xmlns:fx="http://javafx.com/fxml"
|
||||
xmlns="http://javafx.com/javafx"
|
||||
fx:controller="org.cryptomator.ui.vaultoptions.HealthVaultOptionsController"
|
||||
spacing="6">
|
||||
<padding>
|
||||
<Insets topRightBottomLeft="12"/>
|
||||
</padding>
|
||||
<children>
|
||||
<Label text="%vaultOptions.health.introduction" wrapText="true"/>
|
||||
<Label text="%vaultOptions.health.remarks" wrapText="true"/>
|
||||
<GridPane >
|
||||
<padding>
|
||||
<Insets left="6"/>
|
||||
</padding>
|
||||
<columnConstraints>
|
||||
<ColumnConstraints minWidth="20" halignment="LEFT"/>
|
||||
<ColumnConstraints fillWidth="true"/>
|
||||
</columnConstraints>
|
||||
<rowConstraints>
|
||||
<RowConstraints valignment="TOP"/>
|
||||
<RowConstraints valignment="TOP"/>
|
||||
<RowConstraints valignment="TOP"/>
|
||||
</rowConstraints>
|
||||
<Label text="1." GridPane.rowIndex="0" GridPane.columnIndex="0" />
|
||||
<Label text="%vaultOptions.health.remarkSync" wrapText="true" GridPane.rowIndex="0" GridPane.columnIndex="1" />
|
||||
<Label text="2." GridPane.rowIndex="1" GridPane.columnIndex="0" />
|
||||
<Label text="%vaultOptions.health.remarkFix" wrapText="true" GridPane.rowIndex="1" GridPane.columnIndex="1" />
|
||||
<Label text="3." GridPane.rowIndex="2" GridPane.columnIndex="0" />
|
||||
<Label text="%vaultOptions.health.remarkBackup" wrapText="true" GridPane.rowIndex="2" GridPane.columnIndex="1" />
|
||||
</GridPane>
|
||||
<CheckBox text="%vaultOptions.health.affirmation" fx:id="affirmationBox"/>
|
||||
<Button text="%vaultOptions.health.startBtn" disable="${!affirmationBox.selected}" onAction="#startHealthCheck"/>
|
||||
</children>
|
||||
</VBox>
|
||||
@@ -96,7 +96,7 @@ forgetPassword.information=This will delete the saved password of this vault fro
|
||||
forgetPassword.confirmBtn=Forget Password
|
||||
|
||||
# Unlock
|
||||
unlock.title=Unlock Vault
|
||||
unlock.title=Unlock "%s"
|
||||
unlock.passwordPrompt=Enter password for "%s":
|
||||
unlock.savePassword=Remember Password
|
||||
unlock.unlockBtn=Unlock
|
||||
@@ -147,29 +147,38 @@ migration.impossible.reason=The vault cannot be automatically migrated because i
|
||||
migration.impossible.moreInfo=The vault can still be opened with an older version. For instructions on how to manually migrate a vault, visit
|
||||
|
||||
# Health Check
|
||||
health.title=Vault Health Check
|
||||
health.start.configValid=Reading and parsing vault configuration file was successful. Proceed to select checks.
|
||||
health.start.configInvalid=Error while reading and parsing the vault configuration file.
|
||||
health.checkList.header=Available Health Checks
|
||||
health.checkList.selectAllBox=Select All
|
||||
## Start
|
||||
health.title=Health Check of "%s"
|
||||
health.intro.header=Health Check
|
||||
health.intro.text=Health Check is a collection of checks to detect and possibly fix problems in the internal structure of your vault. Please keep in mind:
|
||||
health.intro.remarkSync=Ensure all devices are completely synced, this resolves most problems.
|
||||
health.intro.remarkFix=Not all problems can be fixed.
|
||||
health.intro.remarkBackup=If data is corrupted, only a backup can help.
|
||||
health.intro.affirmation=I have read and understood the above information
|
||||
## Start Failure
|
||||
health.fail.header=Error on loading Vault Configuration
|
||||
health.fail.ioError=An error happened while accessing and reading the config file.
|
||||
health.fail.parseError=An error happened while parsing the vault config.
|
||||
health.fail.moreInfo=More Info
|
||||
## Check Selection
|
||||
health.checkList.description=Select checks in the left list or use the buttons below.
|
||||
health.checkList.selectAllButton=Select All Checks
|
||||
health.checkList.deselectAllButton=Deselect All Checks
|
||||
health.check.runBatchBtn=Run Selected Checks
|
||||
## Detail view
|
||||
health.check.detail.noSelectedCheck=For results select a finished health check in the left list.
|
||||
health.check.detail.header=Results of %s
|
||||
health.check.detail.taskNotStarted=The check was not selected to run.
|
||||
health.check.detail.taskScheduled=The check is scheduled.
|
||||
health.check.detail.taskRunning=The check is currently running…
|
||||
health.check.detail.taskSucceeded=The check finished successfully after %s.
|
||||
health.check.detail.taskFailed=The check exited due to an error.
|
||||
health.check.detail.taskCancelled=The check was cancelled.
|
||||
health.check.detail.problemCount=Found %d problems and %d unfixable errors.
|
||||
health.check.detail.checkScheduled=The check is scheduled.
|
||||
health.check.detail.checkRunning=The check is currently running…
|
||||
health.check.detail.checkSkipped=The check was not selected to run.
|
||||
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.exportBtn=Export Report
|
||||
health.check.fixBtn=Fix
|
||||
health.check.detail.hmsFormat= %d hours, %2d minutes and %2d seconds
|
||||
health.check.detail.msFormat= %d minutes and %2d seconds
|
||||
health.check.detail.sFormat= %d seconds
|
||||
## Checks
|
||||
health.org.cryptomator.cryptofs.health.dirid.DirIdCheck=Directory Check
|
||||
## Fix Application
|
||||
health.fix.fixBtn=Fix
|
||||
health.fix.successTip=Fix successful
|
||||
health.fix.failTip=Fix failed, see log for details
|
||||
|
||||
# Preferences
|
||||
preferences.title=Preferences
|
||||
@@ -310,6 +319,8 @@ vaultOptions.general.actionAfterUnlock=After successful unlock
|
||||
vaultOptions.general.actionAfterUnlock.ignore=Do nothing
|
||||
vaultOptions.general.actionAfterUnlock.reveal=Reveal Drive
|
||||
vaultOptions.general.actionAfterUnlock.ask=Ask
|
||||
vaultOptions.general.startHealthCheckBtn=Start Health Check
|
||||
|
||||
## Mount
|
||||
vaultOptions.mount=Mounting
|
||||
vaultOptions.mount.readonly=Read-Only
|
||||
@@ -328,15 +339,6 @@ vaultOptions.masterkey.forgetSavedPasswordBtn=Forget Saved Password
|
||||
vaultOptions.masterkey.recoveryKeyExplanation=A recovery key is your only means to restore access to a vault if you lose your password.
|
||||
vaultOptions.masterkey.showRecoveryKeyBtn=Display Recovery Key
|
||||
vaultOptions.masterkey.recoverPasswordBtn=Recover Password
|
||||
## Health
|
||||
vaultOptions.health=Health Check
|
||||
vaultOptions.health.startBtn=Start Health Check
|
||||
vaultOptions.health.introduction=Health Check is a collection of checks to detect and possibly fix problems in the internal structure of your vault.
|
||||
vaultOptions.health.remarks=Please keep in mind:
|
||||
vaultOptions.health.remarkSync=Incomplete synchronisation causes most problems. Ensure that every device is completely synced.
|
||||
vaultOptions.health.remarkFix=Not all problems can be fixed.
|
||||
vaultOptions.health.remarkBackup=If data is corrupted, only a backup can help.
|
||||
vaultOptions.health.affirmation=I have read and understood the above information.
|
||||
|
||||
|
||||
# Recovery Key
|
||||
|
||||
Reference in New Issue
Block a user