diff --git a/src/main/java/com/andrewlalis/perfin/control/SqlConsoleViewController.java b/src/main/java/com/andrewlalis/perfin/control/SqlConsoleViewController.java index 312b6a4..20558f2 100644 --- a/src/main/java/com/andrewlalis/perfin/control/SqlConsoleViewController.java +++ b/src/main/java/com/andrewlalis/perfin/control/SqlConsoleViewController.java @@ -1,13 +1,15 @@ package com.andrewlalis.perfin.control; import com.andrewlalis.javafx_scene_router.RouteSelectionListener; +import com.andrewlalis.perfin.data.SavedQueryRepository; import com.andrewlalis.perfin.data.impl.JdbcDataSource; import com.andrewlalis.perfin.model.Profile; import javafx.fxml.FXML; -import javafx.scene.control.ButtonType; -import javafx.scene.control.Dialog; -import javafx.scene.control.DialogPane; -import javafx.scene.control.TextArea; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; import javafx.stage.Modality; import javafx.stage.StageStyle; import javafx.stage.Window; @@ -18,6 +20,7 @@ import java.sql.ResultSet; import java.sql.SQLException; import java.util.Arrays; import java.util.List; +import java.util.Optional; /** * Controller for the SQL Console View, in which the user can write and execute @@ -29,14 +32,16 @@ public class SqlConsoleViewController implements RouteSelectionListener { @FXML public TextArea sqlEditorTextArea; @FXML public TextArea outputTextArea; + @FXML public VBox savedQueriesVBox; @Override public void onRouteSelected(Object context) { sqlEditorTextArea.clear(); outputTextArea.clear(); + refreshSavedQueries(); } - public void executeQuery() { + @FXML public void executeQuery() { String queryText = sqlEditorTextArea.getText().strip(); String[] rawQueries = queryText.split("\\s*;\\s*"); List queries = Arrays.stream(rawQueries) @@ -78,6 +83,70 @@ public class SqlConsoleViewController implements RouteSelectionListener { } } + @FXML public void saveQuery() { + if (sqlEditorTextArea.getText().isBlank()) { + Popups.message(sqlEditorTextArea, "Cannot save an empty query."); + return; + } + TextInputDialog dialog = new TextInputDialog(); + dialog.setTitle("Save Query"); + dialog.setContentText("Enter a name for this query."); + Optional result = dialog.showAndWait(); + if (result.isPresent()) { + SavedQueryRepository repo = Profile.getCurrent().dataSource().getSavedQueryRepository(); + String name = result.get().strip(); + if ( + repo.getSavedQueries().contains(name) && + !Popups.confirm(sqlEditorTextArea, "Are you sure you want to overwrite this saved query?") + ) { + return; + } + String content = sqlEditorTextArea.getText().strip(); + repo.createSavedQuery(name, content); + refreshSavedQueries(); + } + } + + private void refreshSavedQueries() { + savedQueriesVBox.getChildren().clear(); + List savedQueries = Profile.getCurrent().dataSource() + .getSavedQueryRepository().getSavedQueries(); + savedQueriesVBox.getChildren().addAll(savedQueries.stream().map(this::makeQueryTile).toList()); + } + + private Node makeQueryTile(String name) { + AnchorPane pane = new AnchorPane(); + pane.getStyleClass().addAll("tile"); + Label nameLabel = new Label(name); + AnchorPane.setLeftAnchor(nameLabel, 0.0); + AnchorPane.setTopAnchor(nameLabel, 0.0); + AnchorPane.setBottomAnchor(nameLabel, 0.0); + pane.getChildren().add(nameLabel); + + HBox buttonsBox = new HBox(); + buttonsBox.getStyleClass().addAll("std-spacing", "small-font"); + Button loadButton = new Button("Load"); + loadButton.setOnAction(event -> sqlEditorTextArea.setText( + Profile.getCurrent().dataSource().getSavedQueryRepository() + .getSavedQueryContent(name) + )); + buttonsBox.getChildren().add(loadButton); + Button deleteButton = new Button("Delete"); + deleteButton.setOnAction(event -> { + if (Popups.confirm(pane, "Are you sure you want to delete this query?")) { + Profile.getCurrent().dataSource().getSavedQueryRepository().deleteSavedQuery(name); + refreshSavedQueries(); + } + }); + buttonsBox.getChildren().add(deleteButton); + + AnchorPane.setRightAnchor(buttonsBox, 0.0); + AnchorPane.setTopAnchor(buttonsBox, 0.0); + AnchorPane.setBottomAnchor(buttonsBox, 0.0); + pane.getChildren().add(buttonsBox); + return pane; + } + @FXML public void showSchema() { SchemaDialog dialog = new SchemaDialog(sqlEditorTextArea.getScene().getWindow()); dialog.show(); diff --git a/src/main/java/com/andrewlalis/perfin/data/DataSource.java b/src/main/java/com/andrewlalis/perfin/data/DataSource.java index fe8d2b4..d542728 100644 --- a/src/main/java/com/andrewlalis/perfin/data/DataSource.java +++ b/src/main/java/com/andrewlalis/perfin/data/DataSource.java @@ -36,6 +36,7 @@ public interface DataSource { TransactionLineItemRepository getTransactionLineItemRepository(); AttachmentRepository getAttachmentRepository(); HistoryRepository getHistoryRepository(); + SavedQueryRepository getSavedQueryRepository(); AnalyticsRepository getAnalyticsRepository(); @@ -92,6 +93,7 @@ public interface DataSource { TransactionLineItemRepository.class, this::getTransactionLineItemRepository, AttachmentRepository.class, this::getAttachmentRepository, HistoryRepository.class, this::getHistoryRepository, + SavedQueryRepository.class, this::getSavedQueryRepository, AnalyticsRepository.class, this::getAnalyticsRepository ); return (Supplier) repoSuppliers.get(type); diff --git a/src/main/java/com/andrewlalis/perfin/data/SavedQueryRepository.java b/src/main/java/com/andrewlalis/perfin/data/SavedQueryRepository.java new file mode 100644 index 0000000..689f7f5 --- /dev/null +++ b/src/main/java/com/andrewlalis/perfin/data/SavedQueryRepository.java @@ -0,0 +1,10 @@ +package com.andrewlalis.perfin.data; + +import java.util.List; + +public interface SavedQueryRepository extends Repository { + List getSavedQueries(); + String getSavedQueryContent(String name); + void createSavedQuery(String name, String content); + void deleteSavedQuery(String name); +} diff --git a/src/main/java/com/andrewlalis/perfin/data/impl/FileSystemSavedQueryRepository.java b/src/main/java/com/andrewlalis/perfin/data/impl/FileSystemSavedQueryRepository.java new file mode 100644 index 0000000..2490d44 --- /dev/null +++ b/src/main/java/com/andrewlalis/perfin/data/impl/FileSystemSavedQueryRepository.java @@ -0,0 +1,80 @@ +package com.andrewlalis.perfin.data.impl; + +import com.andrewlalis.perfin.data.SavedQueryRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +public record FileSystemSavedQueryRepository(Path contentDir) implements SavedQueryRepository { + private static final Logger log = LoggerFactory.getLogger(FileSystemSavedQueryRepository.class); + + private Path queriesDir() { + return contentDir.resolve("saved-queries"); + } + + private Path queryFile(String name) { + return queriesDir().resolve(name + ".sql"); + } + + @Override + public List getSavedQueries() { + Path dir = queriesDir(); + if (Files.notExists(dir)) return Collections.emptyList(); + try (var stream = Files.list(dir)) { + return stream.filter(p -> + Files.isRegularFile(p) && + p.getFileName().toString().toLowerCase().endsWith(".sql") + ) + .map(p -> { + var s = p.getFileName().toString(); + int idx = s.lastIndexOf('.'); + return s.substring(0, idx); + }) + .sorted() + .toList(); + } catch (IOException e) { + log.error("Failed to list files", e); + return Collections.emptyList(); + } + } + + @Override + public String getSavedQueryContent(String name) { + Path file = queryFile(name); + if (Files.notExists(file)) return null; + try { + return Files.readString(file); + } catch (IOException e) { + log.error("Failed to read saved query content", e); + return null; + } + } + + @Override + public void createSavedQuery(String name, String content) { + try { + if (Files.notExists(queriesDir())) { + Files.createDirectory(queriesDir()); + } + Path file = queryFile(name); + Files.writeString(file, content); + } catch (IOException e) { + log.error("Failed to create saved query.", e); + } + } + + @Override + public void deleteSavedQuery(String name) { + try { + Files.deleteIfExists(queryFile(name)); + } catch (IOException e) { + log.error("Failed to delete saved query."); + } + } +} diff --git a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcDataSource.java b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcDataSource.java index 438cfab..e20092a 100644 --- a/src/main/java/com/andrewlalis/perfin/data/impl/JdbcDataSource.java +++ b/src/main/java/com/andrewlalis/perfin/data/impl/JdbcDataSource.java @@ -74,6 +74,11 @@ public class JdbcDataSource implements DataSource { return new JdbcHistoryRepository(getConnection()); } + @Override + public SavedQueryRepository getSavedQueryRepository() { + return new FileSystemSavedQueryRepository(contentDir); + } + @Override public AnalyticsRepository getAnalyticsRepository() { return new JdbcAnalyticsRepository(getConnection()); diff --git a/src/main/resources/help-pages/sql-console.fxml b/src/main/resources/help-pages/sql-console.fxml index 8e13259..aef097c 100644 --- a/src/main/resources/help-pages/sql-console.fxml +++ b/src/main/resources/help-pages/sql-console.fxml @@ -27,5 +27,11 @@ Prepend your query with `#` or `//` to exclude it from execution. Each comment character disables all query syntax up until the next semicolon, (`;`). + + # Saved Queries # + Click on the **Save Query** button to save the contents of the editor + into a *saved query*, which can then be loaded again at any time. You + can find the saved query files under `/content/saved-queries`, in your + profile's directory. diff --git a/src/main/resources/sql-console-view.fxml b/src/main/resources/sql-console-view.fxml index 6b7d5f7..6ecaf9c 100644 --- a/src/main/resources/sql-console-view.fxml +++ b/src/main/resources/sql-console-view.fxml @@ -3,27 +3,38 @@ - - -