Added saved queries to the SQL Console page.
This commit is contained in:
parent
feda2e1897
commit
ec6bc83353
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue