Added saved queries to the SQL Console page.

This commit is contained in:
Andrew Lalis 2024-07-10 14:35:59 -04:00
parent feda2e1897
commit ec6bc83353
7 changed files with 208 additions and 25 deletions

View File

@ -1,13 +1,15 @@
package com.andrewlalis.perfin.control; package com.andrewlalis.perfin.control;
import com.andrewlalis.javafx_scene_router.RouteSelectionListener; import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
import com.andrewlalis.perfin.data.SavedQueryRepository;
import com.andrewlalis.perfin.data.impl.JdbcDataSource; import com.andrewlalis.perfin.data.impl.JdbcDataSource;
import com.andrewlalis.perfin.model.Profile; import com.andrewlalis.perfin.model.Profile;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.ButtonType; import javafx.scene.Node;
import javafx.scene.control.Dialog; import javafx.scene.control.*;
import javafx.scene.control.DialogPane; import javafx.scene.layout.AnchorPane;
import javafx.scene.control.TextArea; import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Modality; import javafx.stage.Modality;
import javafx.stage.StageStyle; import javafx.stage.StageStyle;
import javafx.stage.Window; import javafx.stage.Window;
@ -18,6 +20,7 @@ import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Optional;
/** /**
* Controller for the SQL Console View, in which the user can write and execute * 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 sqlEditorTextArea;
@FXML public TextArea outputTextArea; @FXML public TextArea outputTextArea;
@FXML public VBox savedQueriesVBox;
@Override @Override
public void onRouteSelected(Object context) { public void onRouteSelected(Object context) {
sqlEditorTextArea.clear(); sqlEditorTextArea.clear();
outputTextArea.clear(); outputTextArea.clear();
refreshSavedQueries();
} }
public void executeQuery() { @FXML public void executeQuery() {
String queryText = sqlEditorTextArea.getText().strip(); String queryText = sqlEditorTextArea.getText().strip();
String[] rawQueries = queryText.split("\\s*;\\s*"); String[] rawQueries = queryText.split("\\s*;\\s*");
List<String> queries = Arrays.stream(rawQueries) List<String> 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<String> 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<String> 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() { @FXML public void showSchema() {
SchemaDialog dialog = new SchemaDialog(sqlEditorTextArea.getScene().getWindow()); SchemaDialog dialog = new SchemaDialog(sqlEditorTextArea.getScene().getWindow());
dialog.show(); dialog.show();

View File

@ -36,6 +36,7 @@ public interface DataSource {
TransactionLineItemRepository getTransactionLineItemRepository(); TransactionLineItemRepository getTransactionLineItemRepository();
AttachmentRepository getAttachmentRepository(); AttachmentRepository getAttachmentRepository();
HistoryRepository getHistoryRepository(); HistoryRepository getHistoryRepository();
SavedQueryRepository getSavedQueryRepository();
AnalyticsRepository getAnalyticsRepository(); AnalyticsRepository getAnalyticsRepository();
@ -92,6 +93,7 @@ public interface DataSource {
TransactionLineItemRepository.class, this::getTransactionLineItemRepository, TransactionLineItemRepository.class, this::getTransactionLineItemRepository,
AttachmentRepository.class, this::getAttachmentRepository, AttachmentRepository.class, this::getAttachmentRepository,
HistoryRepository.class, this::getHistoryRepository, HistoryRepository.class, this::getHistoryRepository,
SavedQueryRepository.class, this::getSavedQueryRepository,
AnalyticsRepository.class, this::getAnalyticsRepository AnalyticsRepository.class, this::getAnalyticsRepository
); );
return (Supplier<R>) repoSuppliers.get(type); return (Supplier<R>) repoSuppliers.get(type);

View File

@ -0,0 +1,10 @@
package com.andrewlalis.perfin.data;
import java.util.List;
public interface SavedQueryRepository extends Repository {
List<String> getSavedQueries();
String getSavedQueryContent(String name);
void createSavedQuery(String name, String content);
void deleteSavedQuery(String name);
}

View File

@ -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<String> 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.");
}
}
}

View File

@ -74,6 +74,11 @@ public class JdbcDataSource implements DataSource {
return new JdbcHistoryRepository(getConnection()); return new JdbcHistoryRepository(getConnection());
} }
@Override
public SavedQueryRepository getSavedQueryRepository() {
return new FileSystemSavedQueryRepository(contentDir);
}
@Override @Override
public AnalyticsRepository getAnalyticsRepository() { public AnalyticsRepository getAnalyticsRepository() {
return new JdbcAnalyticsRepository(getConnection()); return new JdbcAnalyticsRepository(getConnection());

View File

@ -27,5 +27,11 @@
Prepend your query with `#` or `//` to exclude it from execution. Each Prepend your query with `#` or `//` to exclude it from execution. Each
comment character disables all query syntax up until the next semicolon, 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.
</StyledText> </StyledText>
</VBox> </VBox>

View File

@ -3,11 +3,13 @@
<?import javafx.scene.control.*?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.HBox?> <?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
<VBox <HBox
xmlns="http://javafx.com/javafx" xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml" xmlns:fx="http://javafx.com/fxml"
fx:controller="com.andrewlalis.perfin.control.SqlConsoleViewController" fx:controller="com.andrewlalis.perfin.control.SqlConsoleViewController"
styleClass="std-spacing"
> >
<VBox HBox.hgrow="ALWAYS">
<ScrollPane fitToWidth="true" fitToHeight="true" VBox.vgrow="ALWAYS"> <ScrollPane fitToWidth="true" fitToHeight="true" VBox.vgrow="ALWAYS">
<TextArea <TextArea
fx:id="sqlEditorTextArea" fx:id="sqlEditorTextArea"
@ -18,6 +20,7 @@
<HBox styleClass="std-padding,std-spacing"> <HBox styleClass="std-padding,std-spacing">
<Button text="Execute Query" onAction="#executeQuery"/> <Button text="Execute Query" onAction="#executeQuery"/>
<Button text="View Schema" onAction="#showSchema"/> <Button text="View Schema" onAction="#showSchema"/>
<Button text="Save Query" onAction="#saveQuery"/>
</HBox> </HBox>
<ScrollPane fitToHeight="true" fitToWidth="true"> <ScrollPane fitToHeight="true" fitToWidth="true">
<TextArea <TextArea
@ -26,4 +29,12 @@
maxHeight="800" maxHeight="800"
/> />
</ScrollPane> </ScrollPane>
</VBox> </VBox>
<VBox HBox.hgrow="SOMETIMES" minWidth="300" prefWidth="300">
<Label text="Saved Queries"/>
<ScrollPane styleClass="tile-container-scroll" VBox.vgrow="ALWAYS">
<VBox fx:id="savedQueriesVBox" styleClass="tile-container"/>
</ScrollPane>
</VBox>
</HBox>