Skip to content

Commit

Permalink
Add sql log
Browse files Browse the repository at this point in the history
  • Loading branch information
russleyshaw committed Oct 13, 2024
1 parent 7e4b32d commit eb7b919
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 50 deletions.
Binary file modified bun.lockb
Binary file not shown.
51 changes: 26 additions & 25 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,51 +12,52 @@
"tauri:build": "bunx tauri build"
},
"dependencies": {
"@blueprintjs/core": "^5.13.0",
"@blueprintjs/select": "^5.2.4",
"@blueprintjs/core": "^5.13.1",
"@blueprintjs/select": "^5.2.5",
"@monaco-editor/react": "^4.6.0",
"@react-three/fiber": "^8.17.9",
"@tanstack/react-query": "^5.56.2",
"@tanstack/react-query-devtools": "^5.56.2",
"@tanstack/react-router": "^1.58.3",
"@tauri-apps/api": "^2.0.0-rc.0",
"@tauri-apps/plugin-fs": "~2.0.0-rc",
"@tauri-apps/plugin-shell": "~2.0.0-rc",
"@xyflow/react": "^12.3.0",
"@react-three/fiber": "^8.17.10",
"@tanstack/react-query": "^5.59.13",
"@tanstack/react-query-devtools": "^5.59.13",
"@tanstack/react-router": "^1.65.0",
"@tauri-apps/api": "^2.0.2",
"@tauri-apps/plugin-fs": "~2.0.0",
"@tauri-apps/plugin-shell": "~2.0.0",
"@xyflow/react": "^12.3.2",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"framer-motion": "^11.11.1",
"framer-motion": "^11.11.8",
"highlight.js": "^11.10.0",
"jotai": "^2.10.0",
"mobx": "^6.13.2",
"mobx": "^6.13.3",
"mobx-react": "^9.1.1",
"normalize.css": "^8.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-error-boundary": "^4.0.13",
"react-icons": "^5.3.0",
"react-use": "^17.5.1",
"react-virtualized-auto-sizer": "^1.0.24",
"sql-formatter": "^15.4.2",
"sql-formatter": "^15.4.3",
"three": "^0.169.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@biomejs/biome": "1.9.2",
"@tanstack/router-devtools": "^1.58.3",
"@tanstack/router-plugin": "^1.58.4",
"@tauri-apps/cli": "^2.0.0-rc.18",
"@types/bun": "^1.1.10",
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@tanstack/router-devtools": "^1.65.0",
"@tanstack/router-plugin": "^1.65.0",
"@tauri-apps/cli": "^2.0.3",
"@types/bun": "^1.1.11",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"@types/three": "^0.169.0",
"@vitejs/plugin-react": "^4.2.1",
"@vitejs/plugin-react": "^4.3.2",
"autoprefixer": "^10.4.20",
"browserslist": "^4.24.0",
"concurrently": "^9.0.1",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.12",
"typescript": "^5.2.2",
"vite": "^5.3.1",
"tailwindcss": "^3.4.13",
"typescript": "^5.6.3",
"vite": "^5.4.8",
"vite-tsconfig-paths": "^5.0.1"
},
"browserslist": {
Expand Down
113 changes: 113 additions & 0 deletions src/components/SqlLogOutput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { SQL_LOGGER, type SqlLog } from "@/models/SqlLogger";
import { Card, Dialog, DialogBody, H3, HTMLTable, Icon, IconSize } from "@blueprintjs/core";
import { format } from "date-fns";
import hljs from "highlight.js";
import { observer } from "mobx-react";
import { useMemo, useState } from "react";
import * as sqlFormat from "sql-formatter";

import "@/lib/highlight";
import { uniq } from "@/lib/utils";

interface LogDetailDialogProps {
log: SqlLog;
onClose: () => void;
}

const LogDetailDialog = observer(({ log, onClose }: LogDetailDialogProps) => {
const sql = useMemo(
() =>
hljs.highlightAuto(
sqlFormat.format(log.sql, {
tabWidth: 4,
useTabs: false,
language: "postgresql",
}),
).value,
[log.sql],
);

const resultKeys = useMemo(() => {
if (!log.results) return [];

return uniq(log.results.flatMap((r) => Object.keys(r)));
}, [log.results]);
return (
<Dialog style={{ width: "80%", height: "80%" }} title="SQL Log" isOpen onClose={onClose}>
<DialogBody>
<div className="flex flex-col gap-2 p-2">
<div>Created At: {format(log.createdAt, "HH:mm:ss")}</div>
<Card className="overflow-auto">
{/* biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation> */}
<pre className="text-xs m-0" dangerouslySetInnerHTML={{ __html: sql }} />
</Card>

{log.params && (
<Card className="overflow-auto">
<H3>Params</H3>
<pre className="text-xs m-0">{JSON.stringify(log.params, null, 2)}</pre>
</Card>
)}

{log.results && (
<Card className="overflow-auto">
<H3>Results</H3>
<HTMLTable striped>
<thead>
<tr>
{resultKeys.map((key) => (
<th key={key}>{key}</th>
))}
</tr>
</thead>
<tbody>
{log.results.map((row, rowIdx) => (
<tr key={rowIdx}>
{resultKeys.map((key) => (
<td key={key}>{row[key] as any}</td>
))}
</tr>
))}
</tbody>
</HTMLTable>
</Card>
)}
</div>
</DialogBody>
</Dialog>
);
});

interface LogItemProps {
log: SqlLog;
onClick?: () => void;
}

const LogItem = observer(({ log, onClick }: LogItemProps) => {
return (
<div className="even:bg-gray-100/5 p-1 text-xs flex flex-row gap-2" onClick={onClick}>
<span className="text-xs text-gray-500">{format(log.createdAt, "HH:mm:ss")}</span>

{log.status === "idle" && <Icon icon="full-circle" intent="none" size={IconSize.STANDARD} />}
{log.status === "loading" && <Icon icon="full-circle" intent="warning" size={IconSize.STANDARD} />}
{log.status === "success" && <Icon icon="tick-circle" intent="success" size={IconSize.STANDARD} />}
{log.status === "error" && <Icon icon="cross-circle" intent="danger" size={IconSize.STANDARD} />}

<span className="font-mono whitespace-nowrap opacity-75">{log.sql}</span>
</div>
);
});

export const SqlLogOutput = observer(() => {
const [selectedLog, setSelectedLog] = useState<SqlLog | null>(null);
return (
<>
{selectedLog && <LogDetailDialog log={selectedLog} onClose={() => setSelectedLog(null)} />}
<div className="h-40 overflow-y-auto flex flex-col-reverse">
{SQL_LOGGER.logs.map((log, logIdx) => (
<LogItem log={log} key={logIdx} onClick={() => setSelectedLog(log)} />
))}
</div>
</>
);
});
7 changes: 7 additions & 0 deletions src/lib/highlight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Using ES6 import syntax
import hljs from "highlight.js/lib/core";
import sql from "highlight.js/lib/languages/sql";

hljs.registerLanguage("sql", sql);

import "highlight.js/styles/github-dark.css";
7 changes: 7 additions & 0 deletions src/lib/pgsql.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { invoke } from "@tauri-apps/api/core";

import type { ConnectionArgs } from "@/lib/database";
import { SQL_LOGGER } from "@/models/SqlLogger";
import { z } from "zod";
import { UnknownError } from "./error";
import { toMap } from "./utils";
Expand Down Expand Up @@ -29,6 +30,8 @@ export async function executeSql(args: {
sql: string;
params?: unknown[];
}): Promise<ExecuteQueryResult> {
const log = SQL_LOGGER.log(args.sql, args.params ?? []);

const result = (await invoke("pg_execute_query", {
host: args.connection.host,
port: args.connection.port,
Expand All @@ -38,10 +41,14 @@ export async function executeSql(args: {
query: args.sql,
params: args.params ?? [],
}).catch((err) => {
log.error = err.message;
log.status = "error";
console.error("Execute SQL error", err);
if (err instanceof Error) throw err;
throw new UnknownError("Unknown error", err);
})) as ExecuteQueryResult;
log.results = result.rows;
log.status = "success";
console.log("Execute SQL result", args.sql, result);

const colMap = toMap(
Expand Down
28 changes: 28 additions & 0 deletions src/models/SqlLogger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { makeAutoObservable, observable } from "mobx";

export interface SqlLog {
createdAt: Date;
status: "idle" | "loading" | "success" | "error";
sql: string;
params: unknown[];
error?: string;
results?: Record<string, unknown>[];
}

export class SqlLogger {
logs: SqlLog[] = [];

constructor() {
makeAutoObservable(this);
}

log(sql: string, params: unknown[]) {
const log: SqlLog = observable<SqlLog>({ createdAt: new Date(), sql, params, status: "idle" });

this.logs.push(log);

return log;
}
}

export const SQL_LOGGER = new SqlLogger();
19 changes: 11 additions & 8 deletions src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { SqlLogOutput } from "@/components/SqlLogOutput";
import { RootLayout } from "@/layouts/RootLayout";
import { Outlet, createRootRoute } from "@tanstack/react-router";
import { observer } from "mobx-react";

export const Route = createRootRoute({
notFoundComponent: (props) => <div>Not Found</div>,
component: observer(() => {
return (
<RootLayout>
<Outlet />
</RootLayout>
);
}),
component: observer(Component),
});

function Component() {
return (
<RootLayout>
<Outlet />
<SqlLogOutput />
</RootLayout>
);
}
19 changes: 2 additions & 17 deletions src/views/TableBrowserView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import { SqlRowTable } from "@/components/SqlRowTable";
import TrButton from "@/components/TrButton";
import { assertExists } from "@/lib/utils";
import type { ConnectionModel } from "@/models/connection";
import { Button, Callout, H3, HTMLTable, Icon, Tooltip } from "@blueprintjs/core";
import { Editor } from "@monaco-editor/react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { Button, H3, HTMLTable, Icon, Tooltip } from "@blueprintjs/core";
import { useQuery } from "@tanstack/react-query";
import { observer } from "mobx-react";
import { useState } from "react";

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

const [sql, setSql] = useState("");

const executeSqlMutation = useMutation({
mutationFn: async () => {
const data = await connection.executeSql(sql);
return { data, sql };
},
});

const columns = connection.columns.filter((c) => c.table === table && c.schema === schema);

return (
Expand Down Expand Up @@ -80,11 +70,6 @@ export const TableBrowserView = observer(({ connection, table, schema }: TableBr
{useFks ? "Hide FKs" : "Show FKs"}
</Button>
</SqlRowTable>

{executeSqlMutation.error && <Callout intent="danger">{executeSqlMutation.error.message}</Callout>}
<Editor height="40vh" defaultLanguage="sql" value={sql} onChange={(value) => setSql(value ?? "")} />
<Button icon="play" onClick={() => executeSqlMutation.mutate()} />
<Button icon="cross" onClick={() => setSql("")} />
</div>
);
});

0 comments on commit eb7b919

Please sign in to comment.