Compare commits
No commits in common. "83e90430577ed148b371712f45603facc4982e00" and "72d624afdc705fabf8364a4315972c978b8ef390" have entirely different histories.
83e9043057
...
72d624afdc
|
@ -97,7 +97,6 @@ public class PerfinApp extends Application {
|
|||
router.map("categories", PerfinApp.class.getResource("/categories-view.fxml"));
|
||||
router.map("edit-category", PerfinApp.class.getResource("/edit-category.fxml"));
|
||||
router.map("tags", PerfinApp.class.getResource("/tags-view.fxml"));
|
||||
router.map("sql-console", PerfinApp.class.getResource("/sql-console-view.fxml"));
|
||||
|
||||
// Help pages.
|
||||
helpRouter.map("home", PerfinApp.class.getResource("/help-pages/home.fxml"));
|
||||
|
|
|
@ -102,8 +102,4 @@ public class MainViewController {
|
|||
@FXML public void goToDashboard() {
|
||||
router.replace("dashboard");
|
||||
}
|
||||
|
||||
@FXML public void goToSqlConsole() {
|
||||
router.replace("sql-console");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
package com.andrewlalis.perfin.control;
|
||||
|
||||
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
|
||||
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.stage.Modality;
|
||||
import javafx.stage.StageStyle;
|
||||
import javafx.stage.Window;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Controller for the SQL Console View, in which the user can write and execute
|
||||
* arbitrary SQL queries on the database. This allows power users to create
|
||||
* custom analytics queries and get exactly the data they want, without fiddling
|
||||
* with user-friendly search fields.
|
||||
*/
|
||||
public class SqlConsoleViewController implements RouteSelectionListener {
|
||||
|
||||
@FXML public TextArea sqlEditorTextArea;
|
||||
@FXML public TextArea outputTextArea;
|
||||
|
||||
@Override
|
||||
public void onRouteSelected(Object context) {
|
||||
sqlEditorTextArea.clear();
|
||||
outputTextArea.clear();
|
||||
}
|
||||
|
||||
public void executeQuery() {
|
||||
String queryText = sqlEditorTextArea.getText().strip();
|
||||
String[] rawQueries = queryText.split("\\s*;\\s*");
|
||||
List<String> queries = Arrays.stream(rawQueries)
|
||||
.filter(s -> !s.isBlank())
|
||||
.filter(s -> !s.startsWith("#") && !s.startsWith("//"))
|
||||
.toList();
|
||||
outputTextArea.clear();
|
||||
JdbcDataSource dataSource = (JdbcDataSource) Profile.getCurrent().dataSource();
|
||||
try (
|
||||
var conn = dataSource.getConnection();
|
||||
var stmt = conn.createStatement()
|
||||
) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int queryIdx = 0; queryIdx < queries.size(); queryIdx++) {
|
||||
sb.append("Query ").append(queryIdx + 1).append(" of ").append(queries.size()).append(":\n");
|
||||
String query = queries.get(queryIdx);
|
||||
ResultSet rs = stmt.executeQuery(query);
|
||||
int columnCount = rs.getMetaData().getColumnCount();
|
||||
|
||||
for (int i = 1; i <= columnCount; i++) {
|
||||
sb.append(rs.getMetaData().getColumnLabel(i));
|
||||
if (i < columnCount) sb.append(", ");
|
||||
}
|
||||
sb.append('\n');
|
||||
while (rs.next()) {
|
||||
for (int i = 1; i <= columnCount; i++) {
|
||||
sb.append(rs.getString(i));
|
||||
if (i < columnCount) sb.append(", ");
|
||||
}
|
||||
sb.append('\n');
|
||||
}
|
||||
if (queryIdx < queries.size() - 1) {
|
||||
sb.append("-----\n\n");
|
||||
}
|
||||
}
|
||||
outputTextArea.setText(sb.toString());
|
||||
} catch (SQLException e) {
|
||||
outputTextArea.setText("Error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@FXML public void showSchema() {
|
||||
SchemaDialog dialog = new SchemaDialog(sqlEditorTextArea.getScene().getWindow());
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private static class SchemaDialog extends Dialog<Void> {
|
||||
public SchemaDialog(Window owner) {
|
||||
DialogPane pane = new DialogPane();
|
||||
TextArea schemaTextArea = new TextArea();
|
||||
schemaTextArea.setEditable(false);
|
||||
schemaTextArea.getStyleClass().addAll("mono-font", "small-font");
|
||||
try (var in = SqlConsoleViewController.class.getResourceAsStream("/sql/schema.sql")) {
|
||||
if (in == null) throw new IOException("Could not load database schema from resource location.");
|
||||
String schemaStr = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
schemaTextArea.setText(schemaStr);
|
||||
} catch (IOException e) {
|
||||
schemaTextArea.setText("Failed to load schema file!");
|
||||
}
|
||||
pane.setContent(schemaTextArea);
|
||||
pane.getButtonTypes().add(ButtonType.OK);
|
||||
|
||||
initOwner(owner);
|
||||
initModality(Modality.NONE);
|
||||
initStyle(StageStyle.DECORATED);
|
||||
setResizable(true);
|
||||
setTitle("Perfin Database Schema");
|
||||
setDialogPane(pane);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,6 +24,8 @@ import javafx.beans.property.SimpleObjectProperty;
|
|||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.TabPane;
|
||||
import javafx.scene.control.TextArea;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
|
@ -31,6 +33,7 @@ import javafx.scene.layout.VBox;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static com.andrewlalis.perfin.PerfinApp.router;
|
||||
|
@ -50,8 +53,10 @@ public class TransactionsViewController implements RouteSelectionListener {
|
|||
public record RouteContext(Long selectedTransactionId) {}
|
||||
|
||||
@FXML public BorderPane transactionsListBorderPane;
|
||||
@FXML public TabPane searchTabPane;
|
||||
@FXML public TextField searchField;
|
||||
@FXML public AccountSelectionBox filterByAccountComboBox;
|
||||
@FXML public TextArea customSearchSqlTextArea;
|
||||
|
||||
@FXML public VBox transactionsVBox;
|
||||
|
||||
|
@ -75,6 +80,12 @@ public class TransactionsViewController implements RouteSelectionListener {
|
|||
selectedTransaction.set(null);
|
||||
});
|
||||
|
||||
// Add listener to reset search when a different search tab is selected.
|
||||
searchTabPane.getSelectionModel().selectedIndexProperty().addListener((observable, oldValue, newValue) -> {
|
||||
paginationControls.setPage(1);
|
||||
selectedTransaction.set(null);
|
||||
});
|
||||
|
||||
this.paginationControls = new DataSourcePaginationControls(
|
||||
transactionsVBox.getChildren(),
|
||||
new DataSourcePaginationControls.PageFetcherFunction() {
|
||||
|
@ -162,7 +173,24 @@ public class TransactionsViewController implements RouteSelectionListener {
|
|||
Popups.message(transactionsListBorderPane, "Exporting transactions is not yet supported.");
|
||||
}
|
||||
|
||||
@FXML public void executeSqlQuery() {
|
||||
paginationControls.setPage(1);
|
||||
selectedTransaction.set(null);
|
||||
}
|
||||
|
||||
private List<SearchFilter> getCurrentSearchFilters() {
|
||||
if (searchTabPane.getSelectionModel().isSelected(0)) {
|
||||
return getBasicSearchFilters();
|
||||
} else if (searchTabPane.getSelectionModel().isSelected(1)) {
|
||||
return Collections.emptyList();
|
||||
} else if (searchTabPane.getSelectionModel().isSelected(2) && !customSearchSqlTextArea.getText().isBlank()) {
|
||||
var filter = new SearchFilter.Builder().where(customSearchSqlTextArea.getText().strip()).build();
|
||||
return List.of(filter);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private List<SearchFilter> getBasicSearchFilters() {
|
||||
List<SearchFilter> filters = new ArrayList<>();
|
||||
if (searchField.getText() != null && !searchField.getText().isBlank()) {
|
||||
final String text = searchField.getText().strip();
|
||||
|
@ -214,6 +242,13 @@ public class TransactionsViewController implements RouteSelectionListener {
|
|||
return filters;
|
||||
}
|
||||
|
||||
// Temporary utility to try out the new filter builder.
|
||||
private List<SearchFilter> tmpFilter() {
|
||||
return new JdbcTransactionSearcher.FilterBuilder()
|
||||
.byHasLineItems(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
private TransactionTile makeTile(Transaction transaction) {
|
||||
var tile = new TransactionTile(transaction);
|
||||
tile.setOnMouseClicked(event -> {
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
<Button text="Forward" onAction="#goForward"/>
|
||||
<Button text="Dashboard" onAction="#goToDashboard"/>
|
||||
<Button text="Profiles" onAction="#viewProfiles"/>
|
||||
<Button text="SQL Console" onAction="#goToSqlConsole"/>
|
||||
|
||||
<Button text="View Help" fx:id="showManualButton" onAction="#showManual"/>
|
||||
<Button text="Hide Help" fx:id="hideManualButton" onAction="#hideManual"/>
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<VBox
|
||||
xmlns="http://javafx.com/javafx"
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
fx:controller="com.andrewlalis.perfin.control.SqlConsoleViewController"
|
||||
>
|
||||
<ScrollPane fitToWidth="true" fitToHeight="true" VBox.vgrow="ALWAYS">
|
||||
<TextArea
|
||||
fx:id="sqlEditorTextArea"
|
||||
promptText="Write your SQL query here..."
|
||||
styleClass="mono-font,small-font"
|
||||
/>
|
||||
</ScrollPane>
|
||||
<HBox styleClass="std-padding,std-spacing">
|
||||
<Button text="Execute Query" onAction="#executeQuery"/>
|
||||
<Button text="View Schema" onAction="#showSchema"/>
|
||||
</HBox>
|
||||
<ScrollPane fitToHeight="true" fitToWidth="true">
|
||||
<TextArea
|
||||
fx:id="outputTextArea"
|
||||
styleClass="mono-font,small-font"
|
||||
maxHeight="800"
|
||||
/>
|
||||
</ScrollPane>
|
||||
</VBox>
|
|
@ -1,11 +1,3 @@
|
|||
/*
|
||||
+--------------------------+
|
||||
| PERFIN Database Schema |
|
||||
+--------------------------+
|
||||
This file defines the relational database schema for Perfin. Various sections
|
||||
are labeled below.
|
||||
*/
|
||||
|
||||
CREATE TABLE account (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
|
@ -147,8 +139,7 @@ CREATE TABLE balance_record_attachment (
|
|||
ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
|
||||
/* HISTORY ENTITIES */
|
||||
|
||||
/* HISTORY */
|
||||
CREATE TABLE history (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT
|
||||
);
|
||||
|
|
|
@ -21,13 +21,54 @@
|
|||
<HBox>
|
||||
<BorderPane fx:id="transactionsListBorderPane" HBox.hgrow="ALWAYS">
|
||||
<top>
|
||||
<VBox styleClass="padding-extra,std-spacing">
|
||||
<TextField fx:id="searchField" promptText="Search" maxWidth="300" prefWidth="200" minWidth="100"/>
|
||||
<PropertiesPane hgap="5" vgap="5">
|
||||
<Label text="Filter by Account"/>
|
||||
<AccountSelectionBox fx:id="filterByAccountComboBox" allowNone="true" showBalance="false"/>
|
||||
</PropertiesPane>
|
||||
</VBox>
|
||||
<TabPane fx:id="searchTabPane">
|
||||
<Tab text="Basic Search" closable="false">
|
||||
<VBox styleClass="padding-extra,std-spacing">
|
||||
<TextField fx:id="searchField" promptText="Search" maxWidth="300" prefWidth="300" minWidth="100"/>
|
||||
<PropertiesPane hgap="5" vgap="5">
|
||||
<Label text="Filter by Account"/>
|
||||
<AccountSelectionBox fx:id="filterByAccountComboBox" allowNone="true" showBalance="false"/>
|
||||
</PropertiesPane>
|
||||
</VBox>
|
||||
</Tab>
|
||||
|
||||
<Tab text="Advanced Search" closable="false">
|
||||
<Label text="Advanced search filters will go here once they're ready."/>
|
||||
</Tab>
|
||||
|
||||
<Tab text="Custom Search" closable="false">
|
||||
<HBox styleClass="std-spacing,std-padding" maxWidth="600">
|
||||
<VBox styleClass="std-spacing" HBox.hgrow="ALWAYS">
|
||||
<ScrollPane VBox.vgrow="ALWAYS" fitToHeight="true" fitToWidth="true">
|
||||
<TextArea
|
||||
fx:id="customSearchSqlTextArea"
|
||||
promptText="Write your SQL query here..."
|
||||
styleClass="mono-font"
|
||||
maxWidth="Infinity"
|
||||
minHeight="50"
|
||||
prefHeight="150"
|
||||
/>
|
||||
</ScrollPane>
|
||||
<HBox styleClass="std-spacing">
|
||||
<Button text="Execute" onAction="#executeSqlQuery"/>
|
||||
<Button text="View Schema"/>
|
||||
<Button text="Save Query"/>
|
||||
</HBox>
|
||||
</VBox>
|
||||
<VBox HBox.hgrow="NEVER" minWidth="200">
|
||||
<Label text="Saved Queries"/>
|
||||
<ScrollPane VBox.vgrow="ALWAYS">
|
||||
<VBox>
|
||||
<Label text="Query A"/>
|
||||
<Label text="Query B"/>
|
||||
<Label text="Query C"/>
|
||||
<Label text="Query D"/>
|
||||
</VBox>
|
||||
</ScrollPane>
|
||||
</VBox>
|
||||
</HBox>
|
||||
</Tab>
|
||||
</TabPane>
|
||||
</top>
|
||||
<center>
|
||||
<ScrollPane styleClass="tile-container-scroll">
|
||||
|
|
Loading…
Reference in New Issue