Skip to content

Commit eb7b919

Browse files
committed
Add sql log
1 parent 7e4b32d commit eb7b919

File tree

8 files changed

+194
-50
lines changed

8 files changed

+194
-50
lines changed

bun.lockb

9.79 KB
Binary file not shown.

package.json

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,51 +12,52 @@
1212
"tauri:build": "bunx tauri build"
1313
},
1414
"dependencies": {
15-
"@blueprintjs/core": "^5.13.0",
16-
"@blueprintjs/select": "^5.2.4",
15+
"@blueprintjs/core": "^5.13.1",
16+
"@blueprintjs/select": "^5.2.5",
1717
"@monaco-editor/react": "^4.6.0",
18-
"@react-three/fiber": "^8.17.9",
19-
"@tanstack/react-query": "^5.56.2",
20-
"@tanstack/react-query-devtools": "^5.56.2",
21-
"@tanstack/react-router": "^1.58.3",
22-
"@tauri-apps/api": "^2.0.0-rc.0",
23-
"@tauri-apps/plugin-fs": "~2.0.0-rc",
24-
"@tauri-apps/plugin-shell": "~2.0.0-rc",
25-
"@xyflow/react": "^12.3.0",
18+
"@react-three/fiber": "^8.17.10",
19+
"@tanstack/react-query": "^5.59.13",
20+
"@tanstack/react-query-devtools": "^5.59.13",
21+
"@tanstack/react-router": "^1.65.0",
22+
"@tauri-apps/api": "^2.0.2",
23+
"@tauri-apps/plugin-fs": "~2.0.0",
24+
"@tauri-apps/plugin-shell": "~2.0.0",
25+
"@xyflow/react": "^12.3.2",
2626
"clsx": "^2.1.1",
2727
"date-fns": "^4.1.0",
28-
"framer-motion": "^11.11.1",
28+
"framer-motion": "^11.11.8",
29+
"highlight.js": "^11.10.0",
2930
"jotai": "^2.10.0",
30-
"mobx": "^6.13.2",
31+
"mobx": "^6.13.3",
3132
"mobx-react": "^9.1.1",
3233
"normalize.css": "^8.0.1",
33-
"react": "^18.2.0",
34-
"react-dom": "^18.2.0",
34+
"react": "^18.3.1",
35+
"react-dom": "^18.3.1",
3536
"react-error-boundary": "^4.0.13",
3637
"react-icons": "^5.3.0",
3738
"react-use": "^17.5.1",
3839
"react-virtualized-auto-sizer": "^1.0.24",
39-
"sql-formatter": "^15.4.2",
40+
"sql-formatter": "^15.4.3",
4041
"three": "^0.169.0",
4142
"zod": "^3.23.8"
4243
},
4344
"devDependencies": {
4445
"@biomejs/biome": "1.9.2",
45-
"@tanstack/router-devtools": "^1.58.3",
46-
"@tanstack/router-plugin": "^1.58.4",
47-
"@tauri-apps/cli": "^2.0.0-rc.18",
48-
"@types/bun": "^1.1.10",
49-
"@types/react": "^18.2.15",
50-
"@types/react-dom": "^18.2.7",
46+
"@tanstack/router-devtools": "^1.65.0",
47+
"@tanstack/router-plugin": "^1.65.0",
48+
"@tauri-apps/cli": "^2.0.3",
49+
"@types/bun": "^1.1.11",
50+
"@types/react": "^18.3.11",
51+
"@types/react-dom": "^18.3.1",
5152
"@types/three": "^0.169.0",
52-
"@vitejs/plugin-react": "^4.2.1",
53+
"@vitejs/plugin-react": "^4.3.2",
5354
"autoprefixer": "^10.4.20",
5455
"browserslist": "^4.24.0",
5556
"concurrently": "^9.0.1",
5657
"postcss": "^8.4.47",
57-
"tailwindcss": "^3.4.12",
58-
"typescript": "^5.2.2",
59-
"vite": "^5.3.1",
58+
"tailwindcss": "^3.4.13",
59+
"typescript": "^5.6.3",
60+
"vite": "^5.4.8",
6061
"vite-tsconfig-paths": "^5.0.1"
6162
},
6263
"browserslist": {

src/components/SqlLogOutput.tsx

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { SQL_LOGGER, type SqlLog } from "@/models/SqlLogger";
2+
import { Card, Dialog, DialogBody, H3, HTMLTable, Icon, IconSize } from "@blueprintjs/core";
3+
import { format } from "date-fns";
4+
import hljs from "highlight.js";
5+
import { observer } from "mobx-react";
6+
import { useMemo, useState } from "react";
7+
import * as sqlFormat from "sql-formatter";
8+
9+
import "@/lib/highlight";
10+
import { uniq } from "@/lib/utils";
11+
12+
interface LogDetailDialogProps {
13+
log: SqlLog;
14+
onClose: () => void;
15+
}
16+
17+
const LogDetailDialog = observer(({ log, onClose }: LogDetailDialogProps) => {
18+
const sql = useMemo(
19+
() =>
20+
hljs.highlightAuto(
21+
sqlFormat.format(log.sql, {
22+
tabWidth: 4,
23+
useTabs: false,
24+
language: "postgresql",
25+
}),
26+
).value,
27+
[log.sql],
28+
);
29+
30+
const resultKeys = useMemo(() => {
31+
if (!log.results) return [];
32+
33+
return uniq(log.results.flatMap((r) => Object.keys(r)));
34+
}, [log.results]);
35+
return (
36+
<Dialog style={{ width: "80%", height: "80%" }} title="SQL Log" isOpen onClose={onClose}>
37+
<DialogBody>
38+
<div className="flex flex-col gap-2 p-2">
39+
<div>Created At: {format(log.createdAt, "HH:mm:ss")}</div>
40+
<Card className="overflow-auto">
41+
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation> */}
42+
<pre className="text-xs m-0" dangerouslySetInnerHTML={{ __html: sql }} />
43+
</Card>
44+
45+
{log.params && (
46+
<Card className="overflow-auto">
47+
<H3>Params</H3>
48+
<pre className="text-xs m-0">{JSON.stringify(log.params, null, 2)}</pre>
49+
</Card>
50+
)}
51+
52+
{log.results && (
53+
<Card className="overflow-auto">
54+
<H3>Results</H3>
55+
<HTMLTable striped>
56+
<thead>
57+
<tr>
58+
{resultKeys.map((key) => (
59+
<th key={key}>{key}</th>
60+
))}
61+
</tr>
62+
</thead>
63+
<tbody>
64+
{log.results.map((row, rowIdx) => (
65+
<tr key={rowIdx}>
66+
{resultKeys.map((key) => (
67+
<td key={key}>{row[key] as any}</td>
68+
))}
69+
</tr>
70+
))}
71+
</tbody>
72+
</HTMLTable>
73+
</Card>
74+
)}
75+
</div>
76+
</DialogBody>
77+
</Dialog>
78+
);
79+
});
80+
81+
interface LogItemProps {
82+
log: SqlLog;
83+
onClick?: () => void;
84+
}
85+
86+
const LogItem = observer(({ log, onClick }: LogItemProps) => {
87+
return (
88+
<div className="even:bg-gray-100/5 p-1 text-xs flex flex-row gap-2" onClick={onClick}>
89+
<span className="text-xs text-gray-500">{format(log.createdAt, "HH:mm:ss")}</span>
90+
91+
{log.status === "idle" && <Icon icon="full-circle" intent="none" size={IconSize.STANDARD} />}
92+
{log.status === "loading" && <Icon icon="full-circle" intent="warning" size={IconSize.STANDARD} />}
93+
{log.status === "success" && <Icon icon="tick-circle" intent="success" size={IconSize.STANDARD} />}
94+
{log.status === "error" && <Icon icon="cross-circle" intent="danger" size={IconSize.STANDARD} />}
95+
96+
<span className="font-mono whitespace-nowrap opacity-75">{log.sql}</span>
97+
</div>
98+
);
99+
});
100+
101+
export const SqlLogOutput = observer(() => {
102+
const [selectedLog, setSelectedLog] = useState<SqlLog | null>(null);
103+
return (
104+
<>
105+
{selectedLog && <LogDetailDialog log={selectedLog} onClose={() => setSelectedLog(null)} />}
106+
<div className="h-40 overflow-y-auto flex flex-col-reverse">
107+
{SQL_LOGGER.logs.map((log, logIdx) => (
108+
<LogItem log={log} key={logIdx} onClick={() => setSelectedLog(log)} />
109+
))}
110+
</div>
111+
</>
112+
);
113+
});

src/lib/highlight.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Using ES6 import syntax
2+
import hljs from "highlight.js/lib/core";
3+
import sql from "highlight.js/lib/languages/sql";
4+
5+
hljs.registerLanguage("sql", sql);
6+
7+
import "highlight.js/styles/github-dark.css";

src/lib/pgsql.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { invoke } from "@tauri-apps/api/core";
22

33
import type { ConnectionArgs } from "@/lib/database";
4+
import { SQL_LOGGER } from "@/models/SqlLogger";
45
import { z } from "zod";
56
import { UnknownError } from "./error";
67
import { toMap } from "./utils";
@@ -29,6 +30,8 @@ export async function executeSql(args: {
2930
sql: string;
3031
params?: unknown[];
3132
}): Promise<ExecuteQueryResult> {
33+
const log = SQL_LOGGER.log(args.sql, args.params ?? []);
34+
3235
const result = (await invoke("pg_execute_query", {
3336
host: args.connection.host,
3437
port: args.connection.port,
@@ -38,10 +41,14 @@ export async function executeSql(args: {
3841
query: args.sql,
3942
params: args.params ?? [],
4043
}).catch((err) => {
44+
log.error = err.message;
45+
log.status = "error";
4146
console.error("Execute SQL error", err);
4247
if (err instanceof Error) throw err;
4348
throw new UnknownError("Unknown error", err);
4449
})) as ExecuteQueryResult;
50+
log.results = result.rows;
51+
log.status = "success";
4552
console.log("Execute SQL result", args.sql, result);
4653

4754
const colMap = toMap(

src/models/SqlLogger.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { makeAutoObservable, observable } from "mobx";
2+
3+
export interface SqlLog {
4+
createdAt: Date;
5+
status: "idle" | "loading" | "success" | "error";
6+
sql: string;
7+
params: unknown[];
8+
error?: string;
9+
results?: Record<string, unknown>[];
10+
}
11+
12+
export class SqlLogger {
13+
logs: SqlLog[] = [];
14+
15+
constructor() {
16+
makeAutoObservable(this);
17+
}
18+
19+
log(sql: string, params: unknown[]) {
20+
const log: SqlLog = observable<SqlLog>({ createdAt: new Date(), sql, params, status: "idle" });
21+
22+
this.logs.push(log);
23+
24+
return log;
25+
}
26+
}
27+
28+
export const SQL_LOGGER = new SqlLogger();

src/routes/__root.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1+
import { SqlLogOutput } from "@/components/SqlLogOutput";
12
import { RootLayout } from "@/layouts/RootLayout";
23
import { Outlet, createRootRoute } from "@tanstack/react-router";
34
import { observer } from "mobx-react";
45

56
export const Route = createRootRoute({
6-
notFoundComponent: (props) => <div>Not Found</div>,
7-
component: observer(() => {
8-
return (
9-
<RootLayout>
10-
<Outlet />
11-
</RootLayout>
12-
);
13-
}),
7+
component: observer(Component),
148
});
9+
10+
function Component() {
11+
return (
12+
<RootLayout>
13+
<Outlet />
14+
<SqlLogOutput />
15+
</RootLayout>
16+
);
17+
}

src/views/TableBrowserView.tsx

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ import { SqlRowTable } from "@/components/SqlRowTable";
22
import TrButton from "@/components/TrButton";
33
import { assertExists } from "@/lib/utils";
44
import type { ConnectionModel } from "@/models/connection";
5-
import { Button, Callout, H3, HTMLTable, Icon, Tooltip } from "@blueprintjs/core";
6-
import { Editor } from "@monaco-editor/react";
7-
import { useMutation, useQuery } from "@tanstack/react-query";
5+
import { Button, H3, HTMLTable, Icon, Tooltip } from "@blueprintjs/core";
6+
import { useQuery } from "@tanstack/react-query";
87
import { observer } from "mobx-react";
98
import { useState } from "react";
109

@@ -24,15 +23,6 @@ export const TableBrowserView = observer(({ connection, table, schema }: TableBr
2423
},
2524
});
2625

27-
const [sql, setSql] = useState("");
28-
29-
const executeSqlMutation = useMutation({
30-
mutationFn: async () => {
31-
const data = await connection.executeSql(sql);
32-
return { data, sql };
33-
},
34-
});
35-
3626
const columns = connection.columns.filter((c) => c.table === table && c.schema === schema);
3727

3828
return (
@@ -80,11 +70,6 @@ export const TableBrowserView = observer(({ connection, table, schema }: TableBr
8070
{useFks ? "Hide FKs" : "Show FKs"}
8171
</Button>
8272
</SqlRowTable>
83-
84-
{executeSqlMutation.error && <Callout intent="danger">{executeSqlMutation.error.message}</Callout>}
85-
<Editor height="40vh" defaultLanguage="sql" value={sql} onChange={(value) => setSql(value ?? "")} />
86-
<Button icon="play" onClick={() => executeSqlMutation.mutate()} />
87-
<Button icon="cross" onClick={() => setSql("")} />
8873
</div>
8974
);
9075
});

0 commit comments

Comments
 (0)