Compare commits

...

18 Commits

Author SHA1 Message Date
JaniruTEC
3b4ff4d3a2 Merge branch 'develop' into feature/1690-invalid-config-handling 2021-07-06 16:48:07 +02:00
JaniruTEC
1edc6e1189 Removed newline
See: https://github.com/cryptomator/cryptomator/pull/1691#discussion_r650773723
2021-07-06 16:45:39 +02:00
Zane Campbell
97222d3d67 Updated README.md
Co-authored-by: Zane Campbell <development@zappcodestudios.com>
2021-07-06 08:52:16 +02:00
Sebastian Stenzel
172593517a show warning glyph in check list, if check contains non-good results 2021-06-30 17:29:59 +02:00
Sebastian Stenzel
16c64a20e3 reordered
[ci skip]
2021-06-30 17:28:14 +02:00
Sebastian Stenzel
d6f8ab13aa prevent weird window resizing 2021-06-30 16:34:57 +02:00
Sebastian Stenzel
d92a8e7980 class not meant to be part of DI graph
[ci skip]
2021-06-30 16:34:45 +02:00
Sebastian Stenzel
374493e8b4 implemented async sequential fix queue 2021-06-30 14:15:23 +02:00
Sebastian Stenzel
f3953c2fb1 wrap DiagnosticResult in Result in order to track the state of applied fixes 2021-06-30 14:13:05 +02:00
Sebastian Stenzel
5c9c336a33 allow setting glyph to null 2021-06-30 14:08:06 +02:00
Sebastian Stenzel
6b113f26ba reduce Platform.runLater() invokations 2021-06-29 16:26:16 +02:00
Sebastian Stenzel
dbef1466c1 read vault config async 2021-06-29 16:07:46 +02:00
Sebastian Stenzel
6b0d8a48c2 added temporary dummy health checks for testing purposes
[ci skip]
2021-06-29 14:05:01 +02:00
Sebastian Stenzel
c7b9735f13 simplify task selection code 2021-06-29 14:04:32 +02:00
JaniruTEC
714a0c6664 Removed unnecessary super interface
See: https://github.com/cryptomator/cryptomator/pull/1691#discussion_r650770011

Also fixes: https://github.com/cryptomator/cryptomator/pull/1691#discussion_r650769229
2021-06-25 00:17:17 +02:00
JaniruTEC
3762441230 Renamed InvalidSettingsException to InvalidSettingException [skip ci] 2021-06-11 22:52:05 +02:00
JaniruTEC
d0d161023d Added InvalidSettingsException
Added unchecked InvalidSettingsException as indicator for problems with the user config
Added AbstractInvalidSetting as interface for identifiers/handlers for such problems
Added InvalidSetting as enum implementation of AbstractInvalidSetting
2021-06-11 18:54:23 +02:00
JaniruTEC
6cd0fc6807 Updated the contract of the MountPointChooser-interface 2021-06-11 18:30:01 +02:00
23 changed files with 506 additions and 231 deletions

View File

@@ -56,7 +56,7 @@ Download native binaries of Cryptomator on [cryptomator.org](https://cryptomator
- File names get encrypted
- Folder structure gets obfuscated
- Use as many vaults in your Dropbox as you want, each having individual passwords
- Two thousand commits for the security of your data!! :tada:
- Three thousand commits for the security of your data!! :tada:
### Privacy

View File

@@ -34,11 +34,13 @@ import java.util.SortedSet;
* this volume, even if {@code #prepare(Volume, Path)} fails.</i>
*
* <p>If {@code #chooseMountPoint(Volume)} yields no result, the next MPC is executed
* <i>without</i> first calling the {@code #prepare(Volume, Path)} method of the current MPC.
* <i>without</i> calling the {@code #prepare(Volume, Path)} method of the current MPC first.
* This is repeated until<br>
* <ul>
* <li><b>either</b> a mountpoint is returned by {@code #chooseMountPoint(Volume)}
* and {@code #prepare(Volume, Path)} succeeds or fails, ending the entire operation</li>
* <li><b>or</b> {@code #chooseMountPoint(Volume)} throws an exception,
* ending the entire operation</li>
* <li><b>or</b> no MPC remains and an {@link InvalidMountPointException} is thrown.</li>
* </ul>
* If the {@code #prepare(Volume, Path)} method of a MPC fails, the entire
@@ -72,12 +74,13 @@ public interface MountPointChooser {
* Developers should override this method to find or extract a mountpoint for
* the volume <b>without</b> preparing it. Preparation should be done by
* {@link #prepare(Volume, Path)} instead.
* Exceptions in this method should be handled gracefully and result in returning
* {@link Optional#empty()} instead of throwing an exception.
* The Mountpoint-Choosing-Operation will fail if an exception occurs.
* Consequently developers should try to restrict throwing exceptions to those cases where
* aborting the entire operation is sensible. Failure to choose a suitable path should
* be indicated by returning {@link Optional#empty()} instead.
*
* @param caller The Volume that is calling the method to choose a mountpoint
* @return the chosen path or {@link Optional#empty()} if an exception occurred
* or no mountpoint could be found.
* @return the chosen path or {@link Optional#empty()} if no mountpoint could be found.
* @see #isApplicable(Volume)
* @see #prepare(Volume, Path)
*/

View File

@@ -0,0 +1,54 @@
package org.cryptomator.common.settings;
import java.net.URI;
import java.util.Objects;
import java.util.ResourceBundle;
public enum InvalidSetting {
;
private final static String DEFAULT_TRACKER = "https://github.com/cryptomator/cryptomator/issues/";
private final static ResourceBundle BUNDLE = ResourceBundle.getBundle("i18n.strings");
private final static String UNKNOWN_KEY_FORMAT = "<Unknown key: %s>";
private final URI issue;
private final String resourceKey;
InvalidSetting(URI issue, String resourceKey) {
this.issue = issue;
this.resourceKey = Objects.requireNonNull(resourceKey);
}
InvalidSetting(int issueId, String resourceKey) {
this(onDefaultTracker(issueId), Objects.requireNonNull(resourceKey));
}
/**
* Returns the corresponding issue URI of this setting.<br>
* The issue URI usually resolves to a page on the
* <a href="https://github.com/cryptomator/cryptomator/issues">Cryptomator Bugtracker.</a>
*
* @return the issue URI or {@code null} if none is provided.
*/
public URI getIssueURI() {
return this.issue;
}
/**
* Returns a (preferably localized) message, that helps the user understand the
* issue in their configuration and how to fix it.
*
* @return the non-null description of the issue.
*/
public String getMessage() {
if (!BUNDLE.containsKey(this.resourceKey)) {
return UNKNOWN_KEY_FORMAT.formatted(this.resourceKey);
}
return BUNDLE.getString(this.resourceKey);
}
private static URI onDefaultTracker(int id) {
return URI.create(DEFAULT_TRACKER + id);
}
}

View File

@@ -0,0 +1,41 @@
package org.cryptomator.common.settings;
public class InvalidSettingException extends RuntimeException {
private final InvalidSetting reason;
private final String additionalMessage;
public InvalidSettingException(InvalidSetting reason) {
this(reason, null, null);
}
public InvalidSettingException(InvalidSetting reason, String additionalMessage) {
this(reason, additionalMessage, null);
}
public InvalidSettingException(InvalidSetting reason, Throwable cause) {
this(reason, null, cause);
}
public InvalidSettingException(InvalidSetting reason, String additionalMessage, Throwable cause) {
super(composeMessage(reason, additionalMessage), cause);
this.reason = reason;
this.additionalMessage = additionalMessage;
}
public InvalidSetting getReason() {
return this.reason;
}
public String getAdditionalMessage() {
return this.additionalMessage;
}
private static String composeMessage(InvalidSetting reason, String additionalMessage) {
if (additionalMessage == null) {
return reason.getMessage();
}
return reason.getMessage() + "Additionally: " + additionalMessage;
}
}

View File

@@ -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 ObjectProperty<FontAwesome5Icon> glyph = new SimpleObjectProperty<>(this, "glyph", DEFAULT_GLYPH);
private DoubleProperty glyphSize = new SimpleDoubleProperty(this, "glyphSize", DEFAULT_GLYPH_SIZE);
private final ObjectProperty<FontAwesome5Icon> glyph = new SimpleObjectProperty<>(this, "glyph", DEFAULT_GLYPH);
private final DoubleProperty glyphSize = new SimpleDoubleProperty(this, "glyphSize", DEFAULT_GLYPH_SIZE);
static {
try {
@@ -42,7 +42,7 @@ public class FontAwesome5IconView extends Text {
}
private void glyphChanged(@SuppressWarnings("unused") ObservableValue<? extends FontAwesome5Icon> observable, @SuppressWarnings("unused") FontAwesome5Icon oldValue, FontAwesome5Icon newValue) {
setText(newValue.unicode());
setText(newValue == null ? null : newValue.unicode());
}
private void glyphSizeChanged(@SuppressWarnings("unused") ObservableValue<? extends Number> observable, @SuppressWarnings("unused") Number oldValue, Number newValue) {

View File

@@ -16,7 +16,6 @@ public class BatchService extends Service<Void> {
private final Iterator<HealthCheckTask> remainingTasks;
@Inject
public BatchService(Iterable<HealthCheckTask> tasks) {
this.remainingTasks = tasks.iterator();
}

View File

@@ -23,7 +23,7 @@ import java.util.stream.Stream;
@HealthCheckScoped
public class CheckDetailController implements FxController {
private final EasyObservableList<DiagnosticResult> results;
private final EasyObservableList<Result> results;
private final OptionalBinding<Worker.State> taskState;
private final Binding<String> taskName;
private final Binding<String> taskDuration;
@@ -39,7 +39,7 @@ public class CheckDetailController implements FxController {
private final ResultListCellFactory resultListCellFactory;
private final ResourceBundle resourceBundle;
public ListView<DiagnosticResult> resultsListView;
public ListView<Result> resultsListView;
private Subscription resultSubscription;
@Inject
@@ -71,8 +71,8 @@ public class CheckDetailController implements FxController {
}
}
private Function<Stream<? extends DiagnosticResult>, Long> countSeverity(DiagnosticResult.Severity severity) {
return stream -> stream.filter(item -> severity.equals(item.getSeverity())).count();
private Function<Stream<? extends Result>, Long> countSeverity(DiagnosticResult.Severity severity) {
return stream -> stream.filter(item -> severity.equals(item.diagnosis().getSeverity())).count();
}
@FXML

View File

@@ -1,13 +1,10 @@
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.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Worker;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@@ -15,101 +12,60 @@ import javafx.scene.Node;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.ListCell;
import javafx.util.Callback;
import java.util.function.Predicate;
class CheckListCell extends ListCell<HealthCheckTask> {
private final FontAwesome5IconView stateIcon = new FontAwesome5IconView();
private final Callback<HealthCheckTask, BooleanProperty> selectedGetter;
private final ObjectProperty<State> stateProperty;
private CheckBox checkBox = new CheckBox();
private BooleanProperty selectedProperty;
CheckListCell(Callback<HealthCheckTask, BooleanProperty> selectedGetter, ObservableValue<Boolean> switchIndicator) {
this.selectedGetter = selectedGetter;
this.stateProperty = new SimpleObjectProperty<>(State.SELECTION);
switchIndicator.addListener(this::changeState);
CheckListCell() {
setPadding(new Insets(6));
setAlignment(Pos.CENTER_LEFT);
setContentDisplay(ContentDisplay.LEFT);
getStyleClass().add("label");
}
private void changeState(ObservableValue<? extends Boolean> observableValue, boolean oldValue, boolean newValue) {
if (newValue) {
stateProperty.set(State.RUN);
} else {
stateProperty.set(State.SELECTION);
}
}
@Override
protected void updateItem(HealthCheckTask item, boolean empty) {
super.updateItem(item, empty);
if (item != null) {
setText(item.getTitle());
}
switch (stateProperty.get()) {
case SELECTION -> updateItemSelection(item, empty);
case RUN -> updateItemRun(item, empty);
}
}
private void updateItemSelection(HealthCheckTask item, boolean empty) {
if (!empty) {
setGraphic(checkBox);
if (selectedProperty != null) {
checkBox.selectedProperty().unbindBidirectional(selectedProperty);
}
selectedProperty = selectedGetter.call(item);
if (selectedProperty != null) {
checkBox.selectedProperty().bindBidirectional(selectedProperty);
}
} else {
setGraphic(null);
setText(null);
}
}
private void updateItemRun(HealthCheckTask item, boolean empty) {
if (item != null) {
item.stateProperty().addListener(this::stateChanged);
graphicProperty().bind(Bindings.createObjectBinding(() -> graphicForState(item.getState()), item.stateProperty()));
stateIcon.setGlyph(glyphForState(item.getState()));
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 void stateChanged(ObservableValue<? extends Worker.State> observable, Worker.State oldState, Worker.State newState) {
stateIcon.setGlyph(glyphForState(newState));
stateIcon.setVisible(true);
}
private Node graphicForState(Worker.State state) {
return switch (state) {
case READY -> null;
case READY -> checkBox;
case SCHEDULED, RUNNING, FAILED, CANCELLED, SUCCEEDED -> stateIcon;
};
}
private FontAwesome5Icon glyphForState(Worker.State state) {
return switch (state) {
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 -> FontAwesome5Icon.CHECK;
case SUCCEEDED -> checkFoundProblems(item) ? FontAwesome5Icon.EXCLAMATION_TRIANGLE : FontAwesome5Icon.CHECK;
};
}
private enum State {
SELECTION,
RUN;
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);
}
}

View File

@@ -1,6 +1,7 @@
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;
@@ -10,28 +11,24 @@ 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.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
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.cell.CheckBoxListCell;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
@@ -43,6 +40,7 @@ public class CheckListController implements FxController {
private final Stage window;
private final ObservableList<HealthCheckTask> tasks;
private final FilteredList<HealthCheckTask> chosenTasks;
private final ReportWriter reportWriter;
private final ExecutorService executorService;
private final ObjectProperty<HealthCheckTask> selectedTask;
@@ -50,19 +48,18 @@ public class CheckListController implements FxController {
private final SimpleObjectProperty<Worker<?>> runningTask;
private final Binding<Boolean> running;
private final Binding<Boolean> finished;
private final Map<HealthCheckTask, BooleanProperty> listPickIndicators;
private final IntegerProperty numberOfPickedChecks;
private final IntegerBinding chosenTaskCount;
private final BooleanBinding anyCheckSelected;
private final BooleanProperty showResultScreen;
/* FXML */
public ListView<HealthCheckTask> checksListView;
@Inject
public CheckListController(@HealthCheckWindow Stage window, Lazy<Collection<HealthCheckTask>> tasks, ReportWriter reportWriteTask, ObjectProperty<HealthCheckTask> selectedTask, ExecutorService executorService, Lazy<ErrorComponent.Builder> errorComponentBuilder) {
public CheckListController(@HealthCheckWindow Stage window, Lazy<List<HealthCheckTask>> tasks, ReportWriter reportWriteTask, ObjectProperty<HealthCheckTask> selectedTask, ExecutorService executorService, Lazy<ErrorComponent.Builder> errorComponentBuilder) {
this.window = window;
this.tasks = FXCollections.observableArrayList(tasks.get());
this.tasks = FXCollections.observableList(tasks.get(), HealthCheckTask::observables);
this.chosenTasks = this.tasks.filtered(HealthCheckTask::isChosenForExecution);
this.reportWriter = reportWriteTask;
this.executorService = executorService;
this.selectedTask = selectedTask;
@@ -70,13 +67,7 @@ public class CheckListController implements FxController {
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.listPickIndicators = new HashMap<>();
this.numberOfPickedChecks = new SimpleIntegerProperty(0);
this.tasks.forEach(task -> {
var entrySelectedProp = new SimpleBooleanProperty(false);
entrySelectedProp.addListener((observable, oldValue, newValue) -> numberOfPickedChecks.set(numberOfPickedChecks.get() + (newValue ? 1 : -1)));
listPickIndicators.put(task, entrySelectedProp);
});
this.chosenTaskCount = Bindings.size(this.chosenTasks);
this.anyCheckSelected = selectedTask.isNotNull();
this.showResultScreen = new SimpleBooleanProperty(false);
}
@@ -84,27 +75,31 @@ public class CheckListController implements FxController {
@FXML
public void initialize() {
checksListView.setItems(tasks);
checksListView.setCellFactory(view -> new CheckListCell(listPickIndicators::get, showResultScreen));
checksListView.setCellFactory(view -> new CheckListCell());
selectedTask.bind(checksListView.getSelectionModel().selectedItemProperty());
}
@FXML
public void toggleSelectAll(ActionEvent event) {
if (event.getSource() instanceof CheckBox c) {
listPickIndicators.forEach( (task, pickProperty) -> pickProperty.set(c.isSelected()));
tasks.forEach(t -> t.chosenForExecutionProperty().set(c.isSelected()));
}
}
@FXML
public void runSelectedChecks() {
Preconditions.checkState(runningTask.get() == null);
var batch = checksListView.getItems().filtered(item -> listPickIndicators.get(item).get());
var batchService = new BatchService(batch);
// 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(batch.get(0));
checksListView.getSelectionModel().select(chosenTasks.get(0));
checksListView.refresh();
window.sizeToScene();
}
@@ -158,13 +153,12 @@ public class CheckListController implements FxController {
return showResultScreen;
}
public int getNumberOfPickedChecks() {
return numberOfPickedChecks.get();
public int getChosenTaskCount() {
return chosenTaskCount.getValue();
}
public IntegerProperty numberOfPickedChecksProperty() {
return numberOfPickedChecks;
public IntegerBinding chosenTaskCountProperty() {
return chosenTaskCount;
}
}

View File

@@ -0,0 +1,41 @@
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
}
}
}

View File

@@ -19,8 +19,6 @@ import org.cryptomator.ui.keyloading.KeyLoadingComponent;
import org.cryptomator.ui.keyloading.KeyLoadingStrategy;
import org.cryptomator.ui.mainwindow.MainWindow;
import javax.annotation.Nullable;
import javax.inject.Named;
import javax.inject.Provider;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
@@ -30,6 +28,7 @@ import javafx.stage.Modality;
import javafx.stage.Stage;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ResourceBundle;
@@ -65,7 +64,7 @@ abstract class HealthCheckModule {
/* Only inject with Lazy-Wrapper!*/
@Provides
@HealthCheckScoped
static Collection<HealthCheckTask> provideAvailableHealthCheckTasks(Collection<HealthCheck> availableHealthChecks, @HealthCheckWindow Vault vault, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, SecureRandom csprng, ResourceBundle resourceBundle) {
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();
}

View File

@@ -9,7 +9,10 @@ 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;
@@ -32,8 +35,9 @@ class HealthCheckTask extends Task<Void> {
private final Masterkey masterkey;
private final SecureRandom csprng;
private final HealthCheck check;
private final ObservableList<DiagnosticResult> results;
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);
@@ -41,7 +45,7 @@ class HealthCheckTask extends Task<Void> {
this.masterkey = Objects.requireNonNull(masterkey);
this.csprng = Objects.requireNonNull(csprng);
this.check = Objects.requireNonNull(check);
this.results = FXCollections.observableArrayList();
this.results = FXCollections.observableArrayList(Result::observables);
try {
updateTitle(resourceBundle.getString("health." + check.identifier()));
} catch (MissingResourceException e) {
@@ -49,6 +53,7 @@ class HealthCheckTask extends Task<Void> {
updateTitle(check.identifier());
}
this.durationInMillis = new SimpleLongProperty(-1);
this.chosenForExecution = new SimpleBooleanProperty();
}
@Override
@@ -56,25 +61,14 @@ class HealthCheckTask extends Task<Void> {
Instant start = Instant.now();
try (var masterkeyClone = masterkey.clone(); //
var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) {
check.check(vaultPath, vaultConfig, masterkeyClone, cryptor, result -> {
check.check(vaultPath, vaultConfig, masterkeyClone, cryptor, diagnosis -> {
if (isCancelled()) {
throw new CancellationException();
}
// FIXME: slowdown for demonstration purposes only:
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
if (isCancelled()) {
return;
} else {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
Platform.runLater(() -> results.add(result));
Platform.runLater(() -> results.add(Result.create(diagnosis)));
});
}
Platform.runLater(() ->durationInMillis.set(Duration.between(start, Instant.now()).toMillis()));
Platform.runLater(() -> durationInMillis.set(Duration.between(start, Instant.now()).toMillis()));
return null;
}
@@ -90,7 +84,11 @@ class HealthCheckTask extends Task<Void> {
/* Getter */
public ObservableList<DiagnosticResult> results() {
Observable[] observables() {
return new Observable[]{results, chosenForExecution};
}
public ObservableList<Result> results() {
return results;
}
@@ -106,4 +104,11 @@ class HealthCheckTask extends Task<Void> {
return durationInMillis.get();
}
public BooleanProperty chosenForExecutionProperty() {
return chosenForExecution;
}
public boolean isChosenForExecution() {
return chosenForExecution.get();
}
}

View File

@@ -72,7 +72,7 @@ public class ReportWriter {
case SUCCEEDED -> {
writer.write("STATUS: SUCCESS\nRESULTS:\n");
for (var result : task.results()) {
writer.write(REPORT_CHECK_RESULT.formatted(result.getSeverity(), result.toString()));
writer.write(REPORT_CHECK_RESULT.formatted(result.diagnosis().getSeverity(), result.getDescription()));
}
}
case CANCELLED -> writer.write("STATUS: CANCELED\n");

View File

@@ -0,0 +1,43 @@
package org.cryptomator.ui.health;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
record Result(DiagnosticResult diagnosis, ObjectProperty<FixState> fixState) {
enum FixState {
NOT_FIXABLE,
FIXABLE,
FIXING,
FIXED,
FIX_FAILED
}
public static Result create(DiagnosticResult diagnosis) {
FixState initialState = switch (diagnosis.getSeverity()) {
case WARN -> FixState.FIXABLE;
default -> FixState.NOT_FIXABLE;
};
return new Result(diagnosis, new SimpleObjectProperty<>(initialState));
}
public Observable[] observables() {
return new Observable[]{fixState};
}
public String getDescription() {
return diagnosis.toString();
}
public FixState getState() {
return fixState.get();
}
public void setState(FixState state) {
this.fixState.set(state);
}
}

View File

@@ -10,20 +10,24 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.scene.control.Alert;
import javafx.application.Platform;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
@HealthCheckScoped
class ResultFixApplier {
private static final Logger LOG = LoggerFactory.getLogger(ResultFixApplier.class);
private final Path vaultPath;
private final SecureRandom csprng;
private final Masterkey masterkey;
private final VaultConfig vaultConfig;
private final ExecutorService sequentialExecutor;
@Inject
public ResultFixApplier(@HealthCheckWindow Vault vault, AtomicReference<Masterkey> masterkeyRef, AtomicReference<VaultConfig> vaultConfigRef, SecureRandom csprng) {
@@ -31,18 +35,32 @@ class ResultFixApplier {
this.masterkey = masterkeyRef.get();
this.vaultConfig = vaultConfigRef.get();
this.csprng = csprng;
this.sequentialExecutor = Executors.newSingleThreadExecutor();
}
public void fix(DiagnosticResult result) {
Preconditions.checkArgument(result.getSeverity() == DiagnosticResult.Severity.WARN, "Unfixable result");
public CompletionStage<Void> fix(Result result) {
Preconditions.checkArgument(result.getState() == Result.FixState.FIXABLE);
result.setState(Result.FixState.FIXING);
return CompletableFuture.runAsync(() -> fix(result.diagnosis()), sequentialExecutor)
.whenCompleteAsync((unused, throwable) -> {
var fixed = throwable == null ? Result.FixState.FIXED : Result.FixState.FIX_FAILED;
result.setState(fixed);
}, Platform::runLater);
}
public void fix(DiagnosticResult diagnosis) {
Preconditions.checkArgument(diagnosis.getSeverity() == DiagnosticResult.Severity.WARN, "Unfixable result");
try (var masterkeyClone = masterkey.clone(); //
var cryptor = CryptorProvider.forScheme(vaultConfig.getCipherCombo()).provide(masterkeyClone, csprng)) {
result.fix(vaultPath, vaultConfig, masterkeyClone, cryptor);
diagnosis.fix(vaultPath, vaultConfig, masterkeyClone, cryptor);
} catch (Exception e) {
LOG.error("Failed to apply fix", e);
Alert alert = new Alert(Alert.AlertType.ERROR, e.getMessage());
alert.showAndWait();
//TODO: real error/not supported handling
throw new FixFailedException(e);
}
}
public static class FixFailedException extends CompletionException {
private FixFailedException(Throwable cause) {
super(cause);
}
}
}

View File

@@ -1,84 +1,89 @@
package org.cryptomator.ui.health;
import com.tobiasdiez.easybind.EasyBind;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import com.tobiasdiez.easybind.optional.OptionalBinding;
import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.controls.FontAwesome5Icon;
import org.cryptomator.ui.controls.FontAwesome5IconView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.application.Platform;
import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
// unscoped because each cell needs its own controller
public class ResultListCellController implements FxController {
private final ResultFixApplier fixApplier;
private final ObjectProperty<DiagnosticResult> result;
private final Logger LOG = LoggerFactory.getLogger(ResultListCellController.class);
private final ObjectProperty<Result> result;
private final Binding<String> description;
private final ResultFixApplier fixApplier;
private final OptionalBinding<Result.FixState> fixState;
private final ObjectBinding<FontAwesome5Icon> glyph;
private final BooleanBinding fixable;
private final BooleanBinding fixing;
private final BooleanBinding fixed;
public FontAwesome5IconView iconView;
public Button actionButton;
public Button fixButton;
@Inject
public ResultListCellController(ResultFixApplier fixApplier) {
this.result = new SimpleObjectProperty<>(null);
this.description = EasyBind.wrapNullable(result).map(DiagnosticResult::toString).orElse("");
this.description = EasyBind.wrapNullable(result).map(Result::getDescription).orElse("");
this.fixApplier = fixApplier;
result.addListener(this::updateCellContent);
}
private void updateCellContent(ObservableValue<? extends DiagnosticResult> observable, DiagnosticResult oldVal, DiagnosticResult newVal) {
iconView.getStyleClass().clear();
actionButton.setVisible(false);
//TODO: see comment in case WARN
actionButton.setManaged(false);
switch (newVal.getSeverity()) {
case INFO -> {
iconView.setGlyph(FontAwesome5Icon.INFO_CIRCLE);
iconView.getStyleClass().add("glyph-icon-muted");
}
case GOOD -> {
iconView.setGlyph(FontAwesome5Icon.CHECK);
iconView.getStyleClass().add("glyph-icon-primary");
}
case WARN -> {
iconView.setGlyph(FontAwesome5Icon.EXCLAMATION_TRIANGLE);
iconView.getStyleClass().add("glyph-icon-orange");
//TODO: Neither is any fix implemented, nor it is ensured, that only fix is executed at a time with good ui indication
// before both are not fix, do not show the button
//actionButton.setVisible(true);
}
case CRITICAL -> {
iconView.setGlyph(FontAwesome5Icon.TIMES);
iconView.getStyleClass().add("glyph-icon-red");
}
}
this.fixState = EasyBind.wrapNullable(result).mapObservable(Result::fixState);
this.glyph = Bindings.createObjectBinding(this::getGlyph, result);
this.fixable = Bindings.createBooleanBinding(this::isFixable, fixState);
this.fixing = Bindings.createBooleanBinding(this::isFixing, fixState);
this.fixed = Bindings.createBooleanBinding(this::isFixed, fixState);
}
@FXML
public void runResultAction() {
final var realResult = result.get();
if (realResult != null) {
fixApplier.fix(realResult);
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));
}
@FXML
public void fix() {
Result r = result.get();
if (r != null) {
fixApplier.fix(r).whenCompleteAsync(this::fixFinished, Platform::runLater);
}
}
private void fixFinished(Void unused, Throwable exception) {
if (exception != null) {
LOG.error("Failed to apply fix", exception);
// TODO ...
}
}
/* Getter & Setter */
public DiagnosticResult getResult() {
public Result getResult() {
return result.get();
}
public void setResult(DiagnosticResult result) {
public void setResult(Result result) {
this.result.set(result);
}
public ObjectProperty<DiagnosticResult> resultProperty() {
public ObjectProperty<Result> resultProperty() {
return result;
}
@@ -86,7 +91,49 @@ public class ResultListCellController implements FxController {
return description.getValue();
}
public ObjectBinding<FontAwesome5Icon> glyphProperty() {
return glyph;
}
public FontAwesome5Icon getGlyph() {
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;
};
}
public Binding<String> descriptionProperty() {
return description;
}
public BooleanBinding fixableProperty() {
return fixable;
}
public boolean isFixable() {
return fixState.get().map(Result.FixState.FIXABLE::equals).orElse(false);
}
public BooleanBinding fixingProperty() {
return fixing;
}
public boolean isFixing() {
return fixState.get().map(Result.FixState.FIXING::equals).orElse(false);
}
public BooleanBinding fixedProperty() {
return fixed;
}
public boolean isFixed() {
return fixState.get().map(Result.FixState.FIXED::equals).orElse(false);
}
}

View File

@@ -1,7 +1,6 @@
package org.cryptomator.ui.health;
import org.cryptomator.cryptofs.health.api.DiagnosticResult;
import org.cryptomator.ui.common.FxmlLoaderFactory;
import javax.inject.Inject;
@@ -15,7 +14,7 @@ import java.io.IOException;
import java.io.UncheckedIOException;
@HealthCheckScoped
public class ResultListCellFactory implements Callback<ListView<DiagnosticResult>, ListCell<DiagnosticResult>> {
public class ResultListCellFactory implements Callback<ListView<Result>, ListCell<Result>> {
private final FxmlLoaderFactory fxmlLoaders;
@@ -25,7 +24,7 @@ public class ResultListCellFactory implements Callback<ListView<DiagnosticResult
}
@Override
public ListCell<DiagnosticResult> call(ListView<DiagnosticResult> param) {
public ListCell<Result> call(ListView<Result> param) {
try {
FXMLLoader fxmlLoader = fxmlLoaders.load("/fxml/health_result_listcell.fxml");
return new ResultListCellFactory.Cell(fxmlLoader.getRoot(), fxmlLoader.getController());
@@ -34,7 +33,7 @@ public class ResultListCellFactory implements Callback<ListView<DiagnosticResult
}
}
private static class Cell extends ListCell<DiagnosticResult> {
private static class Cell extends ListCell<Result> {
private final Parent node;
private final ResultListCellController controller;
@@ -45,7 +44,7 @@ public class ResultListCellFactory implements Callback<ListView<DiagnosticResult
}
@Override
protected void updateItem(DiagnosticResult item, boolean empty) {
protected void updateItem(Result item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);

View File

@@ -18,11 +18,16 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
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.util.Optional;
import java.io.UncheckedIOException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;
@@ -31,38 +36,40 @@ public class StartController implements FxController {
private static final Logger LOG = LoggerFactory.getLogger(StartController.class);
private final Vault vault;
private final Stage window;
private final Optional<VaultConfig.UnverifiedVaultConfig> unverifiedVaultConfig;
private final CompletableFuture<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;
this.window = window;
this.unverifiedVaultConfig = CompletableFuture.supplyAsync(this::loadConfig, executor);
this.keyLoadingStrategy = keyLoadingStrategy;
this.executor = executor;
this.masterkeyRef = masterkeyRef;
this.vaultConfigRef = vaultConfigRef;
this.checkScene = checkScene;
this.errorComponent = errorComponent;
//TODO: this is ugly
//idea: delay the loading of the vault config and show a spinner (something like "check/load config") and react to the result of the loading
//or: load vault config in a previous step to see if it is loadable.
VaultConfig.UnverifiedVaultConfig tmp;
try {
tmp = vault.getUnverifiedVaultConfig();
} catch (IOException e) {
e.printStackTrace();
tmp = null;
}
this.unverifiedVaultConfig = Optional.ofNullable(tmp);
this.unverifiedVaultConfig.whenCompleteAsync(this::loadedConfig, Platform::runLater);
}
@FXML
@@ -74,41 +81,64 @@ public class StartController implements FxController {
@FXML
public void next() {
LOG.trace("StartController.next()");
executor.submit(this::loadKey);
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.isPresent();
try (var masterkey = keyLoadingStrategy.loadKey(unverifiedVaultConfig.orElseThrow().getKeyId())) {
var unverifiedCfg = unverifiedVaultConfig.get();
assert unverifiedVaultConfig.isDone();
var unverifiedCfg = unverifiedVaultConfig.join();
try (var masterkey = keyLoadingStrategy.loadKey(unverifiedCfg.getKeyId())) {
var verifiedCfg = unverifiedCfg.verify(masterkey.getEncoded(), unverifiedCfg.allegedVaultVersion());
vaultConfigRef.set(verifiedCfg);
var old = masterkeyRef.getAndSet(masterkey.clone());
if (old != null) {
old.destroy();
}
Platform.runLater(this::loadedKey);
} catch (MasterkeyLoadingFailedException e) {
if (keyLoadingStrategy.recoverFromException(e)) {
// retry
loadKey();
} else {
Platform.runLater(() -> loadingKeyFailed(e));
throw new LoadingFailedException(e);
}
} catch (VaultKeyInvalidException e) {
Platform.runLater(() -> loadingKeyFailed(e));
} catch (VaultConfigLoadException e) {
Platform.runLater(() -> loadingKeyFailed(e));
throw new LoadingFailedException(e);
}
}
private void loadedKey() {
LOG.debug("Loaded valid key");
window.setScene(checkScene.get());
private void loadedKey(Void unused, Throwable exception) {
assert Platform.isFxApplicationThread();
if (exception instanceof LoadingFailedException) {
loadingKeyFailed(exception.getCause());
} else if (exception != null) {
loadingKeyFailed(exception);
} else {
LOG.debug("Loaded valid key");
window.setScene(checkScene.get());
}
}
private void loadingKeyFailed(Exception e) {
private void loadingKeyFailed(Throwable e) {
if (e instanceof UnlockCancelledException) {
// ok
} else if (e instanceof VaultKeyInvalidException) {
@@ -120,8 +150,37 @@ public class StartController implements FxController {
}
}
public boolean isInvalidConfig() {
return unverifiedVaultConfig.isEmpty();
/* 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);
}
}
}

View File

@@ -12,6 +12,10 @@ 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;
@@ -23,8 +27,4 @@ public class HealthVaultOptionsController implements FxController {
public void startHealthCheck(ActionEvent event) {
healthCheckWindow.vault(vault).windowToClose(window).build().showHealthCheckWindow();
}
private final Stage window;
private final Vault vault;
private final HealthCheckComponent.Builder healthCheckWindow;
}

View File

@@ -0,0 +1,3 @@
org.cryptomator.ui.health.DummyHealthChecks$DummyCheck1
org.cryptomator.ui.health.DummyHealthChecks$DummyCheck2
org.cryptomator.ui.health.DummyHealthChecks$DummyCheck3

View File

@@ -3,17 +3,18 @@
<?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.control.ListView?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import java.lang.Integer?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.control.CheckBox?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.health.CheckListController"
minHeight="145"
prefWidth="600"
prefHeight="400"
spacing="12">
<padding>
<Insets topRightBottomLeft="12"/>
@@ -28,7 +29,7 @@
<CheckBox onAction="#toggleSelectAll" text="%health.checkList.selectAllBox" visible="${!controller.showResultScreen}" managed="${!controller.showResultScreen}" />
<ListView fx:id="checksListView" VBox.vgrow="ALWAYS"/>
</VBox>
<StackPane visible="${controller.showResultScreen}" managed="${controller.showResultScreen}" HBox.hgrow="ALWAYS" >
<StackPane visible="${controller.showResultScreen}" 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>
@@ -39,7 +40,7 @@
<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.numberOfPickedChecks == ZERO}" visible="${!controller.showResultScreen}" managed="${!controller.showResultScreen}"/>
<Button text="%health.check.runBatchBtn" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#runSelectedChecks" disable="${controller.chosenTaskCount == ZERO}" visible="${!controller.showResultScreen}" managed="${!controller.showResultScreen}"/>
</buttons>
</ButtonBar>
</children>

View File

@@ -6,6 +6,10 @@
<?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"
fx:controller="org.cryptomator.ui.health.ResultListCellController"
@@ -18,12 +22,18 @@
<Insets topRightBottomLeft="6"/>
</padding>
<children>
<FontAwesome5IconView fx:id="iconView" HBox.hgrow="NEVER" glyphSize="16"/>
<FontAwesome5IconView fx:id="iconView" HBox.hgrow="NEVER" glyphSize="16" glyph="${controller.glyph}"/>
<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 -->
<Button fx:id="actionButton" text="%health.check.fixBtn" onAction="#runResultAction" alignment="CENTER" visible="false" minWidth="-Infinity"/>
<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>
</StackPane>
</children>
</HBox>

View File

@@ -17,22 +17,25 @@
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<!-- TODO: combine the two below labels to one and bind the properties accordingly or, preferably think about a new flow -->
<Label text="%health.start.configInvalid" visible="${controller.invalidConfig}" managed="${controller.invalidConfig}" wrapText="true" contentDisplay="LEFT">
<Label text="TODO loading config..." visible="${controller.loading}" managed="${controller.loading}" wrapText="true" contentDisplay="LEFT">
<graphic>
<FontAwesome5IconView glyph="TIMES" styleClass="glyph-icon-red" />
<FontAwesome5IconView glyph="SPINNER"/>
</graphic>
</Label>
<Label text="%health.start.configValid" visible="${!controller.invalidConfig}" managed="${!controller.invalidConfig}" wrapText="true" contentDisplay="LEFT">
<Label text="%health.start.configInvalid" visible="${controller.failed}" managed="${controller.failed}" wrapText="true" contentDisplay="LEFT">
<graphic>
<FontAwesome5IconView glyph="CHECK" styleClass="glyph-icon-primary" />
<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>
<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.invalidConfig}" defaultButton="true" onAction="#next"/>
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" disable="${!controller.loaded}" defaultButton="true" onAction="#next"/>
</buttons>
</ButtonBar>
</children>