diff --git a/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java b/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java index 654caa49e..6151e485e 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java @@ -8,8 +8,12 @@ import io.xpipe.app.core.AppResources; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; +import io.xpipe.app.fxcomps.util.PlatformThread; +import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.issue.ErrorEvent; import javafx.application.Platform; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.value.ObservableValue; import javafx.geometry.Insets; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; @@ -25,21 +29,26 @@ import java.util.function.UnaryOperator; public class MarkdownComp extends Comp> { - private final String markdown; - private final UnaryOperator transformation; + private final ObservableValue markdown; + private final UnaryOperator htmlTransformation; - public MarkdownComp(String markdown, UnaryOperator transformation) { + public MarkdownComp(String markdown, UnaryOperator htmlTransformation) { + this.markdown = new SimpleStringProperty(markdown); + this.htmlTransformation = htmlTransformation; + } + + public MarkdownComp(ObservableValue markdown, UnaryOperator htmlTransformation) { this.markdown = markdown; - this.transformation = transformation; + this.htmlTransformation = htmlTransformation; } private String getHtml() { MutableDataSet options = new MutableDataSet(); Parser parser = Parser.builder(options).build(); HtmlRenderer renderer = HtmlRenderer.builder(options).build(); - Document document = parser.parse(markdown); + Document document = parser.parse(markdown.getValue()); var html = renderer.render(document); - var result = transformation.apply(html); + var result = htmlTransformation.apply(html); return "
" + result + "
"; } @@ -51,15 +60,17 @@ public class MarkdownComp extends Comp> { .orElseThrow(); wv.getEngine().setUserStyleSheetLocation(url.toString()); - // Work around for https://bugs.openjdk.org/browse/JDK-8199014 - try { - var file = Files.createTempFile(null, ".html"); - Files.writeString(file, getHtml()); - var contentUrl = file.toUri(); - wv.getEngine().load(contentUrl.toString()); - } catch (IOException e) { - throw new RuntimeException(e); - } + SimpleChangeListener.apply(PlatformThread.sync(markdown), val -> { + // Work around for https://bugs.openjdk.org/browse/JDK-8199014 + try { + var file = Files.createTempFile(null, ".html"); + Files.writeString(file, getHtml()); + var contentUrl = file.toUri(); + wv.getEngine().load(contentUrl.toString()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); wv.getStyleClass().add("markdown-comp"); addLinkHandler(wv.getEngine()); diff --git a/app/src/main/java/io/xpipe/app/comp/base/MultiStepComp.java b/app/src/main/java/io/xpipe/app/comp/base/MultiStepComp.java index a0d06b5f7..922e3abdc 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/MultiStepComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/MultiStepComp.java @@ -1,11 +1,13 @@ package io.xpipe.app.comp.base; +import atlantafx.base.controls.Spacer; import com.jfoenix.controls.JFXTabPane; import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; +import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.binding.Bindings; import javafx.beans.property.Property; import javafx.beans.property.ReadOnlyProperty; @@ -80,14 +82,6 @@ public abstract class MultiStepComp extends Comp> { }); } - protected void setStartingIndex(int start) { - if (this.entries != null) { - throw new IllegalStateException(); - } - - currentIndex = start; - } - public boolean isCompleted(Entry e) { return entries.indexOf(e) < currentIndex; } @@ -193,23 +187,15 @@ public abstract class MultiStepComp extends Comp> { MultiStepComp comp = this; HBox buttons = new HBox(); + buttons.setFillHeight(true); + buttons.getChildren().add(new Region()); + buttons.getChildren().add(new Spacer()); buttons.getStyleClass().add("buttons"); buttons.setSpacing(5); - var helpButton = new ButtonComp(AppI18n.observable("help"), null, () -> { - getValue().help.run(); - }) - .styleClass("help") - .apply(struc -> struc.get() - .visibleProperty() - .bind(Bindings.createBooleanBinding(() -> getValue().help == null, currentStep))); - if (getValue().help != null) { - buttons.getChildren().add(helpButton.createRegion()); - } - - var spacer = new Region(); - buttons.getChildren().add(spacer); - HBox.setHgrow(spacer, Priority.ALWAYS); + SimpleChangeListener.apply(currentStep, val -> { + buttons.getChildren().set(0, val.bottom() != null ? val.bottom().createRegion() : new Region()); + }); buttons.setAlignment(Pos.CENTER_RIGHT); var nextText = Bindings.createStringBinding( @@ -242,7 +228,7 @@ public abstract class MultiStepComp extends Comp> { var compContent = new JFXTabPane(); compContent.getStyleClass().add("content"); - for (var entry : comp.getEntries()) { + for (var ignored : comp.getEntries()) { compContent.getTabs().add(new Tab(null, null)); } var entryR = comp.getValue().createRegion(); @@ -282,10 +268,8 @@ public abstract class MultiStepComp extends Comp> { public abstract static class Step> extends Comp { - private final Runnable help; - - public Step(Runnable help) { - this.help = help; + public Comp bottom() { + return null; } public void onInit() {} diff --git a/app/src/main/java/io/xpipe/app/comp/base/PopupMenuButtonComp.java b/app/src/main/java/io/xpipe/app/comp/base/PopupMenuButtonComp.java new file mode 100644 index 000000000..8143337d9 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/comp/base/PopupMenuButtonComp.java @@ -0,0 +1,56 @@ +package io.xpipe.app.comp.base; + +import atlantafx.base.controls.Popover; +import atlantafx.base.theme.Styles; +import io.xpipe.app.core.AppFont; +import io.xpipe.app.fxcomps.Comp; +import io.xpipe.app.fxcomps.SimpleComp; +import javafx.beans.value.ObservableValue; +import javafx.scene.control.Button; +import javafx.scene.layout.Region; + +public class PopupMenuButtonComp extends SimpleComp { + + private final ObservableValue name; + private final Comp content; + private final boolean lazy; + + public PopupMenuButtonComp(ObservableValue name, Comp content, boolean lazy) { + this.name = name; + this.content = content; + this.lazy = lazy; + } + + @Override + protected Region createSimple() { + var popover = new Popover(); + if (!lazy) { + popover.setContentNode(content.createRegion()); + } + popover.setCloseButtonEnabled(false); + popover.setHeaderAlwaysVisible(false); + popover.setDetachable(true); + AppFont.small(popover.getContentNode()); + + var extendedDescription = new Button(); + extendedDescription.textProperty().bind(name); + extendedDescription.setMinWidth(Region.USE_PREF_SIZE); + extendedDescription.getStyleClass().add(Styles.BUTTON_OUTLINED); + extendedDescription.getStyleClass().add(Styles.ACCENT); + extendedDescription.getStyleClass().add("long-description"); + extendedDescription.setOnAction(e -> { + if (popover.isShowing()) { + e.consume(); + return; + } + + if (lazy) { + popover.setContentNode(content.createRegion()); + } + + popover.show(extendedDescription); + e.consume(); + }); + return extendedDescription; + } +} diff --git a/app/src/main/java/io/xpipe/app/comp/source/DsDataTransferComp.java b/app/src/main/java/io/xpipe/app/comp/source/DsDataTransferComp.java index fdbf05db9..91ba25a6c 100644 --- a/app/src/main/java/io/xpipe/app/comp/source/DsDataTransferComp.java +++ b/app/src/main/java/io/xpipe/app/comp/source/DsDataTransferComp.java @@ -71,7 +71,7 @@ public class DsDataTransferComp extends SimpleComp { var multi = new MultiStepComp() { @Override protected List setup() { - return List.of(new Entry(null, new Step<>(null) { + return List.of(new Entry(null, new Step<>() { @Override public CompStructure createBase() { return ms.createStructure(); diff --git a/app/src/main/java/io/xpipe/app/comp/source/GuiDsConfigStep.java b/app/src/main/java/io/xpipe/app/comp/source/GuiDsConfigStep.java index 3fa769533..ade2b3bfa 100644 --- a/app/src/main/java/io/xpipe/app/comp/source/GuiDsConfigStep.java +++ b/app/src/main/java/io/xpipe/app/comp/source/GuiDsConfigStep.java @@ -51,7 +51,6 @@ public class GuiDsConfigStep extends MultiStepComp.Step> { Property> currentSource, Property type, BooleanProperty loading) { - super(null); this.input = input; this.baseSource = baseSource; this.currentSource = currentSource; diff --git a/app/src/main/java/io/xpipe/app/comp/source/GuiDsCreatorSaveStep.java b/app/src/main/java/io/xpipe/app/comp/source/GuiDsCreatorSaveStep.java index f58b991ea..16cd12ad5 100644 --- a/app/src/main/java/io/xpipe/app/comp/source/GuiDsCreatorSaveStep.java +++ b/app/src/main/java/io/xpipe/app/comp/source/GuiDsCreatorSaveStep.java @@ -26,7 +26,6 @@ public class GuiDsCreatorSaveStep extends MultiStepComp.Step> { public GuiDsCreatorSaveStep( Property storageGroup, Property dataSourceEntry) { - super(null); this.storageGroup = storageGroup; this.dataSourceEntry = dataSourceEntry; } diff --git a/app/src/main/java/io/xpipe/app/comp/source/GuiDsCreatorTransferStep.java b/app/src/main/java/io/xpipe/app/comp/source/GuiDsCreatorTransferStep.java index 10256d7d4..184955afb 100644 --- a/app/src/main/java/io/xpipe/app/comp/source/GuiDsCreatorTransferStep.java +++ b/app/src/main/java/io/xpipe/app/comp/source/GuiDsCreatorTransferStep.java @@ -35,7 +35,6 @@ public class GuiDsCreatorTransferStep extends MultiStepComp.Step store, ObjectProperty> source) { - super(null); this.targetGroup = targetGroup; this.store = store; this.source = source; diff --git a/app/src/main/java/io/xpipe/app/comp/source/GuiDsStoreSelectStep.java b/app/src/main/java/io/xpipe/app/comp/source/GuiDsStoreSelectStep.java index 09939dc8b..fa13c865c 100644 --- a/app/src/main/java/io/xpipe/app/comp/source/GuiDsStoreSelectStep.java +++ b/app/src/main/java/io/xpipe/app/comp/source/GuiDsStoreSelectStep.java @@ -41,7 +41,6 @@ public class GuiDsStoreSelectStep extends MultiStepComp.Step> baseSource, BooleanProperty loading) { - super(null); this.parent = parent; this.provider = provider; this.input = input; diff --git a/app/src/main/java/io/xpipe/app/comp/source/GuiDsTableMappingConfirmation.java b/app/src/main/java/io/xpipe/app/comp/source/GuiDsTableMappingConfirmation.java index a3a83dbe6..59f3a7fdb 100644 --- a/app/src/main/java/io/xpipe/app/comp/source/GuiDsTableMappingConfirmation.java +++ b/app/src/main/java/io/xpipe/app/comp/source/GuiDsTableMappingConfirmation.java @@ -48,7 +48,7 @@ public class GuiDsTableMappingConfirmation extends SimpleComp { var multi = new MultiStepComp() { @Override protected List setup() { - return List.of(new Entry(null, new Step<>(null) { + return List.of(new Entry(null, new Step<>() { @Override public CompStructure createBase() { return ms.createStructure(); diff --git a/app/src/main/java/io/xpipe/app/comp/source/store/GuiDsStoreCreator.java b/app/src/main/java/io/xpipe/app/comp/source/store/GuiDsStoreCreator.java index 15a2332e8..f1bb3e4ac 100644 --- a/app/src/main/java/io/xpipe/app/comp/source/store/GuiDsStoreCreator.java +++ b/app/src/main/java/io/xpipe/app/comp/source/store/GuiDsStoreCreator.java @@ -2,6 +2,7 @@ package io.xpipe.app.comp.source.store; import io.xpipe.app.comp.base.ErrorOverlayComp; import io.xpipe.app.comp.base.MultiStepComp; +import io.xpipe.app.comp.base.PopupMenuButtonComp; import io.xpipe.app.core.AppExtensionManager; import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppI18n; @@ -22,9 +23,11 @@ import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.util.*; import io.xpipe.core.store.DataStore; import javafx.application.Platform; +import javafx.beans.binding.Bindings; import javafx.beans.property.*; import javafx.geometry.Insets; import javafx.scene.control.Alert; +import javafx.scene.control.ScrollPane; import javafx.scene.control.Separator; import javafx.scene.layout.BorderPane; import javafx.scene.layout.Region; @@ -60,7 +63,6 @@ public class GuiDsStoreCreator extends MultiStepComp.Step> { Predicate filter, String initialName, boolean exists ) { - super(null); this.parent = parent; this.provider = provider; this.store = store; @@ -158,6 +160,16 @@ public class GuiDsStoreCreator extends MultiStepComp.Step> { }); } + @Override + public Comp bottom() { + var disable = Bindings.createBooleanBinding(() -> { + return provider.getValue() == null || store.getValue() == null || !store.getValue().isComplete(); + }, provider, store); + return new PopupMenuButtonComp(new SimpleStringProperty("Insights >"), Comp.of(() -> { + return provider.getValue() != null ? provider.getValue().createInsightsComp(store).createRegion() : null; + }), true).disable(disable).styleClass("button-comp"); + } + private static boolean showInvalidConfirmAlert() { return AppWindowHelper.showBlockingAlert(alert -> { alert.setTitle(AppI18n.get("confirmInvalidStoreTitle")); @@ -197,6 +209,7 @@ public class GuiDsStoreCreator extends MultiStepComp.Step> { private Region createLayout() { var layout = new BorderPane(); + layout.getStyleClass().add("store-creator"); layout.setPadding(new Insets(20)); var providerChoice = new DsStoreProviderChoiceComp(filter, provider); if (provider.getValue() != null) { @@ -217,7 +230,10 @@ public class GuiDsStoreCreator extends MultiStepComp.Step> { var d = n.guiDialog(store); var propVal = new SimpleValidator(); var propR = createStoreProperties(d == null || d.getComp() == null ? null : d.getComp(), propVal); - layout.setCenter(propR); + + var sp = new ScrollPane(propR); + sp.setFitToWidth(true); + layout.setCenter(sp); validator.setValue(new ChainedValidator(List.of( d != null && d.getValidator() != null ? d.getValidator() : new SimpleValidator(), propVal))); diff --git a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java index a9de53b34..d54d6f23e 100644 --- a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java @@ -1,13 +1,17 @@ package io.xpipe.app.ext; +import io.xpipe.app.comp.base.MarkdownComp; import io.xpipe.app.core.AppI18n; +import io.xpipe.app.fxcomps.Comp; import io.xpipe.core.dialog.Dialog; import io.xpipe.core.store.DataStore; import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.ShellStore; import io.xpipe.core.store.StreamDataStore; import io.xpipe.core.util.JacksonizedValue; +import javafx.beans.binding.Bindings; import javafx.beans.property.Property; +import javafx.beans.value.ObservableValue; import java.util.List; @@ -27,6 +31,22 @@ public interface DataStoreProvider { } } + default Comp createInsightsComp(ObservableValue store) { + var content = Bindings.createStringBinding(() -> { + if (store.getValue() == null || !store.getValue().isComplete() || !getStoreClasses().contains(store.getValue().getClass())) { + return null; + } + + return createInsightsMarkdown(store.getValue()); + }, store); + var markdown = new MarkdownComp(content, s -> s).apply(struc -> struc.get().setPrefWidth(450)).apply(struc -> struc.get().setPrefHeight(200)); + return markdown; + } + + default String createInsightsMarkdown(DataStore store) { + return null; + } + default DataCategory getCategory() { var c = getStoreClasses().get(0); if (StreamDataStore.class.isAssignableFrom(c)) { diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/OptionsComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/OptionsComp.java index b00b6f8a0..3859697b6 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/OptionsComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/OptionsComp.java @@ -90,15 +90,17 @@ public class OptionsComp extends Comp> { popover.setDetachable(true); AppFont.small(popover.getContentNode()); - var descriptionHover = new Button("... ?"); - descriptionHover.getStyleClass().add(Styles.BUTTON_OUTLINED); - descriptionHover.getStyleClass().add(Styles.ACCENT); - descriptionHover.setPadding(new Insets(0, 6, 0, 6)); - descriptionHover.getStyleClass().add("long-description"); - AppFont.header(descriptionHover); - descriptionHover.setOnAction(e -> popover.show(descriptionHover)); + var extendedDescription = new Button("... ?"); + extendedDescription.setMinWidth(Region.USE_PREF_SIZE); + extendedDescription.getStyleClass().add(Styles.BUTTON_OUTLINED); + extendedDescription.getStyleClass().add(Styles.ACCENT); + extendedDescription.setPadding(new Insets(0, 6, 0, 6)); + extendedDescription.getStyleClass().add("long-description"); + AppFont.header(extendedDescription); + extendedDescription.setOnAction(e -> popover.show(extendedDescription)); - var descriptionBox = new HBox(description, new Spacer(Orientation.HORIZONTAL), descriptionHover); + var descriptionBox = new HBox(description, new Spacer(Orientation.HORIZONTAL), extendedDescription); + descriptionBox.setSpacing(5); HBox.setHgrow(descriptionBox, Priority.ALWAYS); descriptionBox.setAlignment(Pos.CENTER_LEFT); line.getChildren().add(descriptionBox); diff --git a/app/src/main/resources/io/xpipe/app/resources/style/header-bars.css b/app/src/main/resources/io/xpipe/app/resources/style/header-bars.css index 5bd8641f6..7621aaa0f 100644 --- a/app/src/main/resources/io/xpipe/app/resources/style/header-bars.css +++ b/app/src/main/resources/io/xpipe/app/resources/style/header-bars.css @@ -29,7 +29,7 @@ } .bar .button-comp { --fx-padding: 0.3em 0em 0.3em 0em; +-fx-padding: 0.2em 0em 0.2em 0em; } .collections-bar { diff --git a/app/src/main/resources/io/xpipe/app/resources/style/store-creator.css b/app/src/main/resources/io/xpipe/app/resources/style/store-creator.css new file mode 100644 index 000000000..4e00e7c8d --- /dev/null +++ b/app/src/main/resources/io/xpipe/app/resources/style/store-creator.css @@ -0,0 +1,3 @@ +.store-creator .scroll-pane { + -fx-padding: 0 5px 0 0; + } diff --git a/core/src/main/java/io/xpipe/core/process/ProcessControlProvider.java b/core/src/main/java/io/xpipe/core/process/ProcessControlProvider.java index 123f1b854..61b8dd763 100644 --- a/core/src/main/java/io/xpipe/core/process/ProcessControlProvider.java +++ b/core/src/main/java/io/xpipe/core/process/ProcessControlProvider.java @@ -57,6 +57,14 @@ public abstract class ProcessControlProvider { .orElseThrow(); } + public static String createSshPreview(Object sshStore) { + return INSTANCES.stream() + .map(localProcessControlProvider -> localProcessControlProvider.createSshControlPreview(sshStore)) + .filter(Objects::nonNull) + .findFirst() + .orElseThrow(); + } + public abstract ShellControl sub( ShellControl parent, @NonNull FailableFunction commandFunction, @@ -70,4 +78,6 @@ public abstract class ProcessControlProvider { public abstract ShellControl createLocalProcessControl(boolean stoppable); public abstract ShellControl createSshControl(Object sshStore); + + public abstract String createSshControlPreview(Object sshStore); } diff --git a/core/src/main/java/io/xpipe/core/process/ShellControl.java b/core/src/main/java/io/xpipe/core/process/ShellControl.java index d916a88b3..fd20f0bd9 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellControl.java +++ b/core/src/main/java/io/xpipe/core/process/ShellControl.java @@ -197,7 +197,7 @@ public interface ShellControl extends ProcessControl { default CommandControl command(List command) { return command( - shellProcessControl -> shellProcessControl.getShellDialect().flatten(command)); + shellProcessControl -> ShellDialect.flatten(command)); } void exitAndWait() throws IOException; diff --git a/core/src/main/java/io/xpipe/core/process/ShellDialect.java b/core/src/main/java/io/xpipe/core/process/ShellDialect.java index bde0be502..25915734c 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellDialect.java +++ b/core/src/main/java/io/xpipe/core/process/ShellDialect.java @@ -15,6 +15,16 @@ import java.util.stream.Stream; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") public interface ShellDialect { + public static String flatten(List command) { + return command.stream() + .map(s -> s.contains(" ") + && !(s.startsWith("\"") && s.endsWith("\"")) + && !(s.startsWith("'") && s.endsWith("'")) + ? "\"" + s + "\"" + : s) + .collect(Collectors.joining(" ")); + } + CommandControl prepareTempDirectory(ShellControl shellControl, String directory); String initFileName(ShellControl sc) throws Exception; @@ -67,16 +77,6 @@ public interface ShellDialect { String prepareScriptContent(String content); - default String flatten(List command) { - return command.stream() - .map(s -> s.contains(" ") - && !(s.startsWith("\"") && s.endsWith("\"")) - && !(s.startsWith("'") && s.endsWith("'")) - ? "\"" + s + "\"" - : s) - .collect(Collectors.joining(" ")); - } - default String getExitCommand() { return "exit"; }