@hienlh/ppm 0.6.4 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/web/assets/{chat-tab-CDVCDw_H.js → chat-tab-dwpaSkQD.js} +3 -3
- package/dist/web/assets/{code-editor-wmS73ejX.js → code-editor-ZFl5kZ4-.js} +1 -1
- package/dist/web/assets/database-viewer-DPpOsMqa.js +1 -0
- package/dist/web/assets/{diff-viewer-BsYccTx1.js → diff-viewer-CX74l6lV.js} +1 -1
- package/dist/web/assets/dist-Jb3Tnkpc.js +16 -0
- package/dist/web/assets/{git-graph-BbWb6_Jq.js → git-graph-Dju1rygf.js} +1 -1
- package/dist/web/assets/index-DSg2VjxL.css +2 -0
- package/dist/web/assets/{index-DhuAmTQ1.js → index-DXOEmhRm.js} +6 -6
- package/dist/web/assets/{input-CCCPR1s4.js → input-nI4xe1Y9.js} +1 -1
- package/dist/web/assets/keybindings-store-VhiJwp77.js +1 -0
- package/dist/web/assets/{markdown-renderer-aPdw9BhU.js → markdown-renderer-Bke6DHFh.js} +1 -1
- package/dist/web/assets/postgres-viewer-DaNYnInA.js +1 -0
- package/dist/web/assets/{settings-store-DgOSmeGL.js → settings-store-CfB0vCtQ.js} +1 -1
- package/dist/web/assets/settings-tab-DD05d8rM.js +1 -0
- package/dist/web/assets/sqlite-viewer-Cx7tLyT-.js +1 -0
- package/dist/web/assets/{tab-store-DhXold0e.js → tab-store-DIyJSjtr.js} +1 -1
- package/dist/web/assets/table-DCVKGOr2.js +1 -0
- package/dist/web/assets/{terminal-tab-3tDV4RCn.js → terminal-tab-_farMLMO.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-Ccqh1RD4.js → use-monaco-theme-Dexl3s3E.js} +1 -1
- package/dist/web/index.html +8 -8
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/server/routes/chat.ts +2 -2
- package/src/server/routes/database.ts +5 -3
- package/src/services/database/sqlite-adapter.ts +1 -2
- package/src/services/sqlite.service.ts +2 -1
- package/src/types/config.ts +10 -2
- package/src/web/components/chat/tool-cards.tsx +2 -2
- package/src/web/components/database/connection-list.tsx +82 -33
- package/src/web/components/database/database-sidebar.tsx +2 -13
- package/src/web/components/database/database-viewer.tsx +232 -0
- package/src/web/components/database/use-connections.ts +14 -21
- package/src/web/components/database/use-database.ts +120 -0
- package/src/web/components/layout/command-palette.tsx +4 -5
- package/src/web/components/layout/editor-panel.tsx +1 -0
- package/src/web/components/layout/mobile-nav.tsx +1 -1
- package/src/web/components/layout/sidebar.tsx +1 -2
- package/src/web/components/layout/tab-bar.tsx +1 -0
- package/src/web/components/layout/tab-content.tsx +5 -0
- package/src/web/components/postgres/postgres-viewer.tsx +38 -30
- package/src/web/components/postgres/use-postgres.ts +2 -5
- package/src/web/components/settings/ai-settings-section.tsx +0 -1
- package/src/web/components/sqlite/sqlite-viewer.tsx +24 -18
- package/src/web/components/sqlite/use-sqlite.ts +4 -4
- package/src/web/hooks/use-chat.ts +1 -1
- package/src/web/hooks/use-usage.ts +1 -1
- package/src/web/stores/tab-store.ts +1 -0
- package/dist/web/assets/dist-PpKqMvyx.js +0 -16
- package/dist/web/assets/index-aIGuIMQ8.css +0 -2
- package/dist/web/assets/keybindings-store-BqgrTQAC.js +0 -1
- package/dist/web/assets/postgres-viewer-V4hKmmzV.js +0 -1
- package/dist/web/assets/settings-tab-DwsKpk9T.js +0 -1
- package/dist/web/assets/sqlite-viewer-BRsj8GXc.js +0 -1
- /package/dist/web/assets/{api-client-BHpHp5Lz.js → api-client-4Ni0i4Hl.js} +0 -0
- /package/dist/web/assets/{react-l9v2XLcs.js → react-DHSo28we.js} +0 -0
- /package/dist/web/assets/{utils-CAPYyGV3.js → utils-siJJ3uG0.js} +0 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
|
+
import { api } from "@/lib/api-client";
|
|
3
|
+
|
|
4
|
+
export interface DbTableInfo { name: string; schema: string; rowCount: number }
|
|
5
|
+
export interface DbColumnInfo { name: string; type: string; nullable: boolean; pk: boolean; defaultValue: string | null }
|
|
6
|
+
export interface DbQueryResult { columns: string[]; rows: Record<string, unknown>[]; rowsAffected: number; changeType: "select" | "modify" }
|
|
7
|
+
interface DbTableData { columns: string[]; rows: Record<string, unknown>[]; total: number; page: number; limit: number }
|
|
8
|
+
|
|
9
|
+
/** SessionStorage cache key for table data */
|
|
10
|
+
function cacheKey(connectionId: number, table: string, schema: string, page: number) {
|
|
11
|
+
return `ppm-db-${connectionId}-${schema}.${table}-p${page}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function readCache(connectionId: number, table: string, schema: string, page: number): { data: DbTableData; cols: DbColumnInfo[] } | null {
|
|
15
|
+
try {
|
|
16
|
+
const raw = sessionStorage.getItem(cacheKey(connectionId, table, schema, page));
|
|
17
|
+
return raw ? JSON.parse(raw) : null;
|
|
18
|
+
} catch { return null; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function writeCache(connectionId: number, table: string, schema: string, page: number, data: DbTableData, cols: DbColumnInfo[]) {
|
|
22
|
+
try { sessionStorage.setItem(cacheKey(connectionId, table, schema, page), JSON.stringify({ data, cols })); } catch { /* quota */ }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generic database hook for unified API (/api/db/connections/:id/...).
|
|
27
|
+
* Works for any DB type (postgres, sqlite, mysql, etc.) via adapter pattern.
|
|
28
|
+
* No auto-fetch on mount — viewer calls selectTable() to start loading.
|
|
29
|
+
*/
|
|
30
|
+
export function useDatabase(connectionId: number) {
|
|
31
|
+
const base = `/api/db/connections/${connectionId}`;
|
|
32
|
+
const [selectedTable, setSelectedTable] = useState<string | null>(null);
|
|
33
|
+
const [selectedSchema, setSelectedSchema] = useState("public");
|
|
34
|
+
const [tableData, setTableData] = useState<DbTableData | null>(null);
|
|
35
|
+
const [schema, setSchema] = useState<DbColumnInfo[]>([]);
|
|
36
|
+
const [loading, setLoading] = useState(true);
|
|
37
|
+
const [error, setError] = useState<string | null>(null);
|
|
38
|
+
const [page, setPageState] = useState(1);
|
|
39
|
+
const [queryResult, setQueryResult] = useState<DbQueryResult | null>(null);
|
|
40
|
+
const [queryError, setQueryError] = useState<string | null>(null);
|
|
41
|
+
const [queryLoading, setQueryLoading] = useState(false);
|
|
42
|
+
|
|
43
|
+
// Fetch table data + schema for current selection
|
|
44
|
+
const fetchTableData = useCallback(async (table?: string, tableSchema?: string, p?: number) => {
|
|
45
|
+
const t = table ?? selectedTable;
|
|
46
|
+
const s = tableSchema ?? selectedSchema;
|
|
47
|
+
if (!t) return;
|
|
48
|
+
setLoading(true);
|
|
49
|
+
try {
|
|
50
|
+
const [data, cols] = await Promise.all([
|
|
51
|
+
api.get<DbTableData>(`${base}/data?table=${encodeURIComponent(t)}&schema=${s}&page=${p ?? page}&limit=100`),
|
|
52
|
+
api.get<DbColumnInfo[]>(`${base}/schema?table=${encodeURIComponent(t)}&schema=${s}`),
|
|
53
|
+
]);
|
|
54
|
+
setTableData(data);
|
|
55
|
+
setSchema(cols);
|
|
56
|
+
writeCache(connectionId, t, s, p ?? page, data, cols);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
setError((e as Error).message);
|
|
59
|
+
} finally {
|
|
60
|
+
setLoading(false);
|
|
61
|
+
}
|
|
62
|
+
}, [base, connectionId, selectedTable, selectedSchema, page]);
|
|
63
|
+
|
|
64
|
+
const selectTable = useCallback((name: string, tableSchema = "public") => {
|
|
65
|
+
setSelectedTable(name);
|
|
66
|
+
setSelectedSchema(tableSchema);
|
|
67
|
+
setPageState(1);
|
|
68
|
+
setQueryResult(null);
|
|
69
|
+
// Show cached data instantly, then refresh in background
|
|
70
|
+
const cached = readCache(connectionId, name, tableSchema, 1);
|
|
71
|
+
if (cached) {
|
|
72
|
+
setTableData(cached.data);
|
|
73
|
+
setSchema(cached.cols);
|
|
74
|
+
setLoading(false);
|
|
75
|
+
// Still fetch fresh data in background
|
|
76
|
+
fetchTableData(name, tableSchema, 1);
|
|
77
|
+
} else {
|
|
78
|
+
fetchTableData(name, tableSchema, 1);
|
|
79
|
+
}
|
|
80
|
+
}, [connectionId, fetchTableData]);
|
|
81
|
+
|
|
82
|
+
const changePage = useCallback((p: number) => {
|
|
83
|
+
setPageState(p);
|
|
84
|
+
fetchTableData(undefined, undefined, p);
|
|
85
|
+
}, [fetchTableData]);
|
|
86
|
+
|
|
87
|
+
const executeQuery = useCallback(async (sqlText: string) => {
|
|
88
|
+
setQueryLoading(true);
|
|
89
|
+
setQueryError(null);
|
|
90
|
+
try {
|
|
91
|
+
const result = await api.post<DbQueryResult>(`${base}/query`, { sql: sqlText });
|
|
92
|
+
setQueryResult(result);
|
|
93
|
+
if (result.changeType === "modify") fetchTableData(selectedTable ?? undefined, selectedSchema);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
setQueryError((e as Error).message);
|
|
96
|
+
} finally {
|
|
97
|
+
setQueryLoading(false);
|
|
98
|
+
}
|
|
99
|
+
}, [base, selectedTable, selectedSchema, fetchTableData]);
|
|
100
|
+
|
|
101
|
+
const updateCell = useCallback(async (pkColumn: string, pkValue: unknown, column: string, value: unknown) => {
|
|
102
|
+
if (!selectedTable) return;
|
|
103
|
+
const t = selectedTable;
|
|
104
|
+
const s = selectedSchema;
|
|
105
|
+
try {
|
|
106
|
+
await api.put(`${base}/cell`, { table: t, schema: s, pkColumn, pkValue, column, value });
|
|
107
|
+
// Re-fetch with explicit args to avoid stale closure
|
|
108
|
+
fetchTableData(t, s);
|
|
109
|
+
} catch (e) {
|
|
110
|
+
setError((e as Error).message);
|
|
111
|
+
}
|
|
112
|
+
}, [base, selectedTable, selectedSchema, fetchTableData]);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
selectedTable, selectTable, tableData, schema,
|
|
116
|
+
loading, error, page, setPage: changePage,
|
|
117
|
+
queryResult, queryError, queryLoading, executeQuery,
|
|
118
|
+
updateCell, refreshData: fetchTableData,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -119,9 +119,8 @@ export function CommandPalette({ open, onClose, initialQuery = "" }: { open: boo
|
|
|
119
119
|
if (isPathQuery(query) || query.trim().length < 2) { setDbResults([]); return; }
|
|
120
120
|
const timer = setTimeout(async () => {
|
|
121
121
|
try {
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
setDbResults(json.ok ? (json.data ?? []) : []);
|
|
122
|
+
const data = await api.get<DbSearchResult[]>(`/api/db/search?q=${encodeURIComponent(query.trim())}`);
|
|
123
|
+
setDbResults(data ?? []);
|
|
125
124
|
} catch { setDbResults([]); }
|
|
126
125
|
}, 300);
|
|
127
126
|
return () => clearTimeout(timer);
|
|
@@ -219,11 +218,11 @@ export function CommandPalette({ open, onClose, initialQuery = "" }: { open: boo
|
|
|
219
218
|
connectionColor: r.connectionColor,
|
|
220
219
|
action: () => {
|
|
221
220
|
openTab({
|
|
222
|
-
type:
|
|
221
|
+
type: "database",
|
|
223
222
|
title: `${r.connectionName} · ${r.tableName}`,
|
|
224
223
|
projectId: null,
|
|
225
224
|
closable: true,
|
|
226
|
-
metadata: { connectionId: r.connectionId, tableName: r.tableName, schemaName: r.schemaName, connectionColor: r.connectionColor },
|
|
225
|
+
metadata: { connectionId: r.connectionId, connectionName: r.connectionName, dbType: r.connectionType, tableName: r.tableName, schemaName: r.schemaName, connectionColor: r.connectionColor },
|
|
227
226
|
});
|
|
228
227
|
onClose();
|
|
229
228
|
},
|
|
@@ -17,6 +17,7 @@ const TAB_COMPONENTS: Record<TabType, React.LazyExoticComponent<React.ComponentT
|
|
|
17
17
|
terminal: lazy(() => import("@/components/terminal/terminal-tab").then((m) => ({ default: m.TerminalTab }))),
|
|
18
18
|
chat: lazy(() => import("@/components/chat/chat-tab").then((m) => ({ default: m.ChatTab }))),
|
|
19
19
|
editor: lazy(() => import("@/components/editor/code-editor").then((m) => ({ default: m.CodeEditor }))),
|
|
20
|
+
database: lazy(() => import("@/components/database/database-viewer").then((m) => ({ default: m.DatabaseViewer }))),
|
|
20
21
|
sqlite: lazy(() => import("@/components/sqlite/sqlite-viewer").then((m) => ({ default: m.SqliteViewer }))),
|
|
21
22
|
postgres: lazy(() => import("@/components/postgres/postgres-viewer").then((m) => ({ default: m.PostgresViewer }))),
|
|
22
23
|
"git-graph": lazy(() => import("@/components/git/git-graph").then((m) => ({ default: m.GitGraph }))),
|
|
@@ -21,7 +21,7 @@ const NEW_TAB_OPTIONS: { type: TabType; label: string }[] = [
|
|
|
21
21
|
const NEW_TAB_LABELS: Partial<Record<TabType, string>> = Object.fromEntries(NEW_TAB_OPTIONS.map((o) => [o.type, o.label]));
|
|
22
22
|
|
|
23
23
|
const TAB_ICONS: Record<TabType, React.ElementType> = {
|
|
24
|
-
terminal: Terminal, chat: MessageSquare, editor: FileCode, sqlite: Database, postgres: Database,
|
|
24
|
+
terminal: Terminal, chat: MessageSquare, editor: FileCode, database: Database, sqlite: Database, postgres: Database,
|
|
25
25
|
"git-graph": GitBranch, "git-diff": FileDiff, settings: Settings,
|
|
26
26
|
};
|
|
27
27
|
|
|
@@ -97,8 +97,7 @@ export function Sidebar() {
|
|
|
97
97
|
: "border-transparent text-text-secondary hover:text-foreground",
|
|
98
98
|
)}
|
|
99
99
|
>
|
|
100
|
-
<Icon className="size-3.5" />
|
|
101
|
-
{sidebarWidth >= 240 && <span>{tab.label}</span>}
|
|
100
|
+
<Icon className="size-3.5" title={tab.label} />
|
|
102
101
|
</button>
|
|
103
102
|
);
|
|
104
103
|
})}
|
|
@@ -18,6 +18,11 @@ const TAB_COMPONENTS: Record<TabType, React.LazyExoticComponent<React.ComponentT
|
|
|
18
18
|
default: m.CodeEditor,
|
|
19
19
|
})),
|
|
20
20
|
),
|
|
21
|
+
database: lazy(() =>
|
|
22
|
+
import("@/components/database/database-viewer").then((m) => ({
|
|
23
|
+
default: m.DatabaseViewer,
|
|
24
|
+
})),
|
|
25
|
+
),
|
|
21
26
|
sqlite: lazy(() =>
|
|
22
27
|
import("@/components/sqlite/sqlite-viewer").then((m) => ({
|
|
23
28
|
default: m.SqliteViewer,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useCallback, useMemo } from "react";
|
|
1
|
+
import { useState, useCallback, useMemo, useEffect, useRef } from "react";
|
|
2
2
|
import { Database, Loader2, AlertCircle, Play, ChevronLeft, ChevronRight, Table, RefreshCw } from "lucide-react";
|
|
3
3
|
import { useReactTable, getCoreRowModel, flexRender, type ColumnDef } from "@tanstack/react-table";
|
|
4
4
|
import CodeMirror from "@uiw/react-codemirror";
|
|
@@ -15,7 +15,7 @@ export function PostgresViewer({ metadata }: Props) {
|
|
|
15
15
|
// When connectionId present, the hook auto-connects — skip connection form
|
|
16
16
|
if (!pg.connected) return <ConnectionForm initialValue={initialConn} onConnect={pg.connect} loading={pg.loading} error={pg.error} />;
|
|
17
17
|
|
|
18
|
-
return <ConnectedView pg={pg} initialTable={metadata?.tableName as string | undefined} />;
|
|
18
|
+
return <ConnectedView pg={pg} initialTable={metadata?.tableName as string | undefined} hideTableList={!!connectionId} connectionName={metadata?.connectionName as string | undefined} />;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/* ---------- Connection Form ---------- */
|
|
@@ -44,45 +44,53 @@ function ConnectionForm({ initialValue, onConnect, loading, error }: {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/* ---------- Connected View ---------- */
|
|
47
|
-
function ConnectedView({ pg, initialTable }: { pg: ReturnType<typeof usePostgres>; initialTable?: string }) {
|
|
47
|
+
function ConnectedView({ pg, initialTable, hideTableList, connectionName }: { pg: ReturnType<typeof usePostgres>; initialTable?: string; hideTableList?: boolean; connectionName?: string }) {
|
|
48
48
|
const [queryPanelOpen, setQueryPanelOpen] = useState(false);
|
|
49
49
|
|
|
50
|
-
// Jump to initial table
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
|
|
50
|
+
// Jump to initial table — when hideTableList, go direct (skip table list fetch)
|
|
51
|
+
const didInit = useRef(false);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (!initialTable || didInit.current) return;
|
|
54
|
+
if (hideTableList && pg.connected) {
|
|
55
|
+
didInit.current = true;
|
|
56
|
+
pg.selectTable(initialTable);
|
|
57
|
+
} else if (pg.tables.length > 0) {
|
|
58
|
+
const t = pg.tables.find((t) => t.name === initialTable);
|
|
59
|
+
if (t) { didInit.current = true; pg.selectTable(t.name, t.schema); }
|
|
60
|
+
}
|
|
61
|
+
}, [initialTable, pg.connected, pg.tables]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
56
62
|
|
|
57
63
|
return (
|
|
58
64
|
<div className="flex h-full w-full overflow-hidden">
|
|
59
|
-
{/* Table sidebar */}
|
|
60
|
-
|
|
61
|
-
<div className="
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
</div>
|
|
67
|
-
<div className="flex-1 overflow-y-auto">
|
|
68
|
-
{pg.tables.map((t) => (
|
|
69
|
-
<button key={`${t.schema}.${t.name}`} type="button" onClick={() => pg.selectTable(t.name, t.schema)}
|
|
70
|
-
className={`w-full flex items-center gap-2 px-3 py-1.5 text-left text-xs transition-colors ${
|
|
71
|
-
pg.selectedTable === t.name ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted/50 hover:text-foreground"}`}>
|
|
72
|
-
<Table className="size-3 shrink-0" />
|
|
73
|
-
<span className="truncate flex-1">{t.schema !== "public" ? `${t.schema}.` : ""}{t.name}</span>
|
|
74
|
-
<span className="text-[10px] opacity-60">{t.rowCount}</span>
|
|
65
|
+
{/* Table sidebar — hidden when opened from database sidebar */}
|
|
66
|
+
{!hideTableList && (
|
|
67
|
+
<div className="w-48 shrink-0 flex flex-col bg-background overflow-hidden">
|
|
68
|
+
<div className="flex items-center justify-between px-3 py-2 border-b border-border">
|
|
69
|
+
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Tables</span>
|
|
70
|
+
<button type="button" onClick={pg.refreshTables} className="text-muted-foreground hover:text-foreground transition-colors" title="Refresh">
|
|
71
|
+
<RefreshCw className="size-3" />
|
|
75
72
|
</button>
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
</div>
|
|
74
|
+
<div className="flex-1 overflow-y-auto">
|
|
75
|
+
{pg.tables.map((t) => (
|
|
76
|
+
<button key={`${t.schema}.${t.name}`} type="button" onClick={() => pg.selectTable(t.name, t.schema)}
|
|
77
|
+
className={`w-full flex items-center gap-2 px-3 py-1.5 text-left text-xs transition-colors ${
|
|
78
|
+
pg.selectedTable === t.name ? "bg-muted text-foreground" : "text-muted-foreground hover:bg-muted/50 hover:text-foreground"}`}>
|
|
79
|
+
<Table className="size-3 shrink-0" />
|
|
80
|
+
<span className="truncate flex-1">{t.schema !== "public" ? `${t.schema}.` : ""}{t.name}</span>
|
|
81
|
+
<span className="text-[10px] opacity-60">{t.rowCount}</span>
|
|
82
|
+
</button>
|
|
83
|
+
))}
|
|
84
|
+
{pg.tables.length === 0 && <p className="px-3 py-4 text-xs text-muted-foreground text-center">No tables found</p>}
|
|
85
|
+
</div>
|
|
78
86
|
</div>
|
|
79
|
-
|
|
87
|
+
)}
|
|
80
88
|
|
|
81
89
|
{/* Main area */}
|
|
82
|
-
<div className=
|
|
90
|
+
<div className={`flex-1 flex flex-col overflow-hidden ${!hideTableList ? "border-l border-border" : ""}`}>
|
|
83
91
|
<div className="flex items-center gap-2 px-3 py-1.5 border-b border-border bg-background shrink-0">
|
|
84
92
|
<Database className="size-3.5 text-muted-foreground" />
|
|
85
|
-
<span className="text-xs text-muted-foreground truncate">PostgreSQL</span>
|
|
93
|
+
<span className="text-xs text-muted-foreground truncate">{connectionName ?? "PostgreSQL"}</span>
|
|
86
94
|
{pg.selectedTable && <span className="text-xs text-muted-foreground">/ {pg.selectedTable}</span>}
|
|
87
95
|
<div className="ml-auto">
|
|
88
96
|
<button type="button" onClick={() => setQueryPanelOpen((v) => !v)}
|
|
@@ -50,12 +50,9 @@ export function usePostgres(connectionId?: number) {
|
|
|
50
50
|
if (unifiedBase) {
|
|
51
51
|
setLoading(true);
|
|
52
52
|
try {
|
|
53
|
-
|
|
53
|
+
// Use cached tables (no live DB query) — sidebar handles sync
|
|
54
|
+
const data = await api.get<PgTableInfo[]>(`${unifiedBase}/tables?cached=1`);
|
|
54
55
|
setTables(data);
|
|
55
|
-
if (data.length > 0 && !selectedTable) {
|
|
56
|
-
setSelectedTable(data[0]!.name);
|
|
57
|
-
setSelectedSchema(data[0]!.schema ?? "public");
|
|
58
|
-
}
|
|
59
56
|
} catch (e) { setError((e as Error).message); }
|
|
60
57
|
finally { setLoading(false); }
|
|
61
58
|
return;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
2
|
import { Database, Loader2, AlertCircle } from "lucide-react";
|
|
3
3
|
import { useSqlite } from "./use-sqlite";
|
|
4
4
|
import { SqliteTableList } from "./sqlite-table-list";
|
|
@@ -24,9 +24,11 @@ export function SqliteViewer({ metadata }: SqliteViewerProps) {
|
|
|
24
24
|
projectName=""
|
|
25
25
|
dbPath=""
|
|
26
26
|
connectionId={connectionId}
|
|
27
|
+
connectionName={metadata?.connectionName as string | undefined}
|
|
27
28
|
initialTable={initialTable}
|
|
28
29
|
queryPanelOpen={queryPanelOpen}
|
|
29
30
|
onToggleQueryPanel={() => setQueryPanelOpen((v) => !v)}
|
|
31
|
+
hideTableList
|
|
30
32
|
/>
|
|
31
33
|
);
|
|
32
34
|
}
|
|
@@ -50,19 +52,21 @@ export function SqliteViewer({ metadata }: SqliteViewerProps) {
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
function SqliteViewerInner({
|
|
53
|
-
projectName, dbPath, connectionId, initialTable, queryPanelOpen, onToggleQueryPanel,
|
|
55
|
+
projectName, dbPath, connectionId, connectionName, initialTable, queryPanelOpen, onToggleQueryPanel, hideTableList,
|
|
54
56
|
}: {
|
|
55
|
-
projectName: string; dbPath: string; connectionId?: number; initialTable?: string;
|
|
56
|
-
queryPanelOpen: boolean; onToggleQueryPanel: () => void;
|
|
57
|
+
projectName: string; dbPath: string; connectionId?: number; connectionName?: string; initialTable?: string;
|
|
58
|
+
queryPanelOpen: boolean; onToggleQueryPanel: () => void; hideTableList?: boolean;
|
|
57
59
|
}) {
|
|
58
60
|
const sqlite = useSqlite(projectName, dbPath, connectionId);
|
|
59
61
|
|
|
60
62
|
// Jump to initial table from sidebar click
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
const didInit = useRef(false);
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (initialTable && !didInit.current && sqlite.tables.length > 0) {
|
|
66
|
+
didInit.current = true;
|
|
67
|
+
sqlite.selectTable(initialTable);
|
|
68
|
+
}
|
|
69
|
+
}, [initialTable, sqlite.tables]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
66
70
|
|
|
67
71
|
if (sqlite.error && sqlite.tables.length === 0) {
|
|
68
72
|
return (
|
|
@@ -84,20 +88,22 @@ function SqliteViewerInner({
|
|
|
84
88
|
|
|
85
89
|
return (
|
|
86
90
|
<div className="flex h-full w-full overflow-hidden">
|
|
87
|
-
{/* Left sidebar — table list */}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
{/* Left sidebar — table list (hidden when opened from database sidebar) */}
|
|
92
|
+
{!hideTableList && (
|
|
93
|
+
<SqliteTableList
|
|
94
|
+
tables={sqlite.tables}
|
|
95
|
+
selectedTable={sqlite.selectedTable}
|
|
96
|
+
onSelect={sqlite.selectTable}
|
|
97
|
+
onRefresh={sqlite.refreshTables}
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
94
100
|
|
|
95
101
|
{/* Main content area */}
|
|
96
|
-
<div className=
|
|
102
|
+
<div className={`flex-1 flex flex-col overflow-hidden ${!hideTableList ? "border-l border-border" : ""}`}>
|
|
97
103
|
{/* Toolbar */}
|
|
98
104
|
<div className="flex items-center gap-2 px-3 py-1.5 border-b border-border bg-background shrink-0">
|
|
99
105
|
<Database className="size-3.5 text-muted-foreground" />
|
|
100
|
-
<span className="text-xs text-muted-foreground truncate">{dbPath}</span>
|
|
106
|
+
<span className="text-xs text-muted-foreground truncate">{connectionName ?? dbPath}</span>
|
|
101
107
|
<span className="text-xs text-muted-foreground">
|
|
102
108
|
{sqlite.selectedTable && `/ ${sqlite.selectedTable}`}
|
|
103
109
|
</span>
|
|
@@ -23,21 +23,21 @@ export function useSqlite(projectName: string, dbPath: string, connectionId?: nu
|
|
|
23
23
|
const base = unifiedBase ?? `${projectUrl(projectName)}/sqlite`;
|
|
24
24
|
const qs = unifiedBase ? "" : `path=${encodeURIComponent(dbPath)}`;
|
|
25
25
|
|
|
26
|
-
// Fetch tables on mount
|
|
26
|
+
// Fetch tables on mount — use cache when connectionId (sidebar handles live sync)
|
|
27
27
|
const fetchTables = useCallback(async () => {
|
|
28
28
|
setLoading(true);
|
|
29
29
|
setError(null);
|
|
30
30
|
try {
|
|
31
|
-
const qsPart = qs ? `?${qs}` : "";
|
|
31
|
+
const qsPart = unifiedBase ? "?cached=1" : qs ? `?${qs}` : "";
|
|
32
32
|
const data = await api.get<TableInfo[]>(`${base}/tables${qsPart}`);
|
|
33
33
|
setTables(data);
|
|
34
|
-
if (data.length > 0 && !selectedTable) setSelectedTable(data[0]!.name);
|
|
34
|
+
if (!unifiedBase && data.length > 0 && !selectedTable) setSelectedTable(data[0]!.name);
|
|
35
35
|
} catch (e) {
|
|
36
36
|
setError((e as Error).message);
|
|
37
37
|
} finally {
|
|
38
38
|
setLoading(false);
|
|
39
39
|
}
|
|
40
|
-
}, [base, qs]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
40
|
+
}, [base, qs, unifiedBase]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
41
41
|
|
|
42
42
|
useEffect(() => { fetchTables(); }, [fetchTables]);
|
|
43
43
|
|
|
@@ -33,7 +33,7 @@ interface UseChatReturn {
|
|
|
33
33
|
isConnected: boolean;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export function useChat(sessionId: string | null, providerId = "claude
|
|
36
|
+
export function useChat(sessionId: string | null, providerId = "claude", projectName = ""): UseChatReturn {
|
|
37
37
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
38
38
|
const [messagesLoading, setMessagesLoading] = useState(false);
|
|
39
39
|
const [isStreaming, setIsStreaming] = useState(false);
|
|
@@ -12,7 +12,7 @@ interface UseUsageReturn {
|
|
|
12
12
|
refreshUsage: () => void;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function useUsage(projectName: string, providerId = "claude
|
|
15
|
+
export function useUsage(projectName: string, providerId = "claude"): UseUsageReturn {
|
|
16
16
|
const [usageInfo, setUsageInfo] = useState<UsageInfo>({});
|
|
17
17
|
const [usageLoading, setUsageLoading] = useState(false);
|
|
18
18
|
const [lastFetchedAt, setLastFetchedAt] = useState<string | null>(null);
|