@hienlh/ppm 0.11.16 → 0.11.18
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 +16 -0
- package/dist/web/assets/{ai-settings-section-C6FDY8qE.js → ai-settings-section-L6XAmZEP.js} +1 -1
- package/dist/web/assets/{api-client-Bn-Pi9k5.js → api-client-CwbMRXYl.js} +1 -1
- package/dist/web/assets/{api-settings-C__hxGX2.js → api-settings-ByUGHhTB.js} +1 -1
- package/dist/web/assets/{audio-preview-DjLLuzUD.js → audio-preview-CZ7uVdJ1.js} +1 -1
- package/dist/web/assets/{chat-tab-CL9_hlGE.js → chat-tab-D0edjxcO.js} +4 -4
- package/dist/web/assets/{code-editor-YaQFb5Db.js → code-editor-CHWEBvuT.js} +2 -2
- package/dist/web/assets/{conflict-editor-Dk6MS9pE.js → conflict-editor-BsNC4CIp.js} +1 -1
- package/dist/web/assets/database-viewer-dYaf9tXG.js +2 -0
- package/dist/web/assets/{diff-viewer-DPgNFO4j.js → diff-viewer-BlQsizFF.js} +1 -1
- package/dist/web/assets/{extension-webview-B8361xT-.js → extension-webview-DXjdd0U6.js} +1 -1
- package/dist/web/assets/{image-preview-B8oAGvHG.js → image-preview-eH8PNO1D.js} +1 -1
- package/dist/web/assets/{index-Bl3W1FYm.js → index-BVQikaUv.js} +6 -6
- package/dist/web/assets/index-C99i-AFP.css +2 -0
- package/dist/web/assets/keybindings-store-BIQHClUy.js +1 -0
- package/dist/web/assets/{keybindings-store-C9KsBH7z.js → keybindings-store-CThBg3hS.js} +1 -1
- package/dist/web/assets/{markdown-renderer-B8Gp0FKa.js → markdown-renderer-CyDOVFYo.js} +1 -1
- package/dist/web/assets/{pdf-preview-D7JrByDM.js → pdf-preview-BOJHsWt2.js} +1 -1
- package/dist/web/assets/{port-forwarding-tab-D2w0zW8n.js → port-forwarding-tab-BxuO01Zb.js} +1 -1
- package/dist/web/assets/{postgres-viewer-ei8XhyiT.js → postgres-viewer-cLp_WazA.js} +1 -1
- package/dist/web/assets/{project-store-BYmQ0fDC.js → project-store-IB6pAGQh.js} +1 -1
- package/dist/web/assets/{settings-store-B9axDbuA.js → settings-store-fDOEursg.js} +2 -2
- package/dist/web/assets/settings-tab-BQpEkd-V.js +1 -0
- package/dist/web/assets/{sql-query-editor-BnpKNGG0.js → sql-query-editor-JwymAmuK.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-BNkPwKNL.js → sqlite-viewer-BgEY7Fml.js} +1 -1
- package/dist/web/assets/{terminal-tab-DwYueV8K.js → terminal-tab-B6meTG0M.js} +1 -1
- package/dist/web/assets/{use-blob-url-BSltfg79.js → use-blob-url-BU9hYOj9.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-BERjR8IA.js → use-monaco-theme-o7Ip-BDL.js} +1 -1
- package/dist/web/assets/{video-preview-BnZt6xtr.js → video-preview-D2gRujKO.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/database.ts +11 -3
- package/src/services/postgres.service.ts +1 -1
- package/src/services/supervisor.ts +7 -2
- package/src/web/components/database/connection-list.tsx +7 -2
- package/src/web/components/database/data-grid.tsx +1 -1
- package/src/web/components/database/database-sidebar.tsx +2 -1
- package/src/web/components/database/use-connections.ts +24 -11
- package/src/web/components/layout/upgrade-banner.tsx +47 -37
- package/src/web/lib/api-client.ts +6 -1
- package/dist/web/assets/database-viewer-BO58nGFL.js +0 -2
- package/dist/web/assets/index-B0V_IYbX.css +0 -2
- package/dist/web/assets/keybindings-store-BkZjvU9J.js +0 -1
- package/dist/web/assets/settings-tab-Dd4mGQHq.js +0 -1
package/package.json
CHANGED
|
@@ -9,6 +9,14 @@ import { syncTables, searchTables, getTablesFromCache } from "../../services/tab
|
|
|
9
9
|
import { isReadOnlyQuery } from "../../services/database/readonly-check.ts";
|
|
10
10
|
import { ok, err } from "../../types/api.ts";
|
|
11
11
|
|
|
12
|
+
/** Race a promise against a timeout — ensures routes always respond */
|
|
13
|
+
function withTimeout<T>(promise: Promise<T>, ms: number, message = "Connection timed out"): Promise<T> {
|
|
14
|
+
return Promise.race([
|
|
15
|
+
promise,
|
|
16
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error(message)), ms)),
|
|
17
|
+
]);
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
export const databaseRoutes = new Hono();
|
|
13
21
|
|
|
14
22
|
/** Strip sensitive connection_config from connection responses */
|
|
@@ -190,7 +198,7 @@ databaseRoutes.post("/test", async (c) => {
|
|
|
190
198
|
const body = await c.req.json<{ type: "sqlite" | "postgres"; connectionConfig: { type: string; path?: string; connectionString?: string } }>();
|
|
191
199
|
if (!body.type || !body.connectionConfig) return c.json(err("type and connectionConfig required"), 400);
|
|
192
200
|
const adapter = getAdapter(body.type);
|
|
193
|
-
const result = await adapter.testConnection(body.connectionConfig as import("../../types/database.ts").DbConnectionConfig);
|
|
201
|
+
const result = await withTimeout(adapter.testConnection(body.connectionConfig as import("../../types/database.ts").DbConnectionConfig), 15_000);
|
|
194
202
|
return c.json(ok(result));
|
|
195
203
|
} catch (e) {
|
|
196
204
|
return c.json(err((e as Error).message), 500);
|
|
@@ -204,7 +212,7 @@ databaseRoutes.post("/connections/:id/test", async (c) => {
|
|
|
204
212
|
if (!conn) return c.json(err("Connection not found"), 404);
|
|
205
213
|
const config = decryptConfig(conn.connection_config);
|
|
206
214
|
const adapter = getAdapter(conn.type);
|
|
207
|
-
const result = await adapter.testConnection(config);
|
|
215
|
+
const result = await withTimeout(adapter.testConnection(config), 15_000);
|
|
208
216
|
return c.json(ok(result));
|
|
209
217
|
} catch (e) {
|
|
210
218
|
return c.json(err((e as Error).message), 500);
|
|
@@ -217,7 +225,7 @@ databaseRoutes.get("/connections/:id/tables", async (c) => {
|
|
|
217
225
|
const conn = resolveConn(c.req.param("id"));
|
|
218
226
|
if (!conn) return c.json(err("Connection not found"), 404);
|
|
219
227
|
const useCached = c.req.query("cached") === "1";
|
|
220
|
-
const result = useCached ? getTablesFromCache(conn.id) : await syncTables(conn.id);
|
|
228
|
+
const result = useCached ? getTablesFromCache(conn.id) : await withTimeout(syncTables(conn.id), 15_000);
|
|
221
229
|
const tables = result.map((t) => ({ name: t.tableName, schema: t.schemaName, rowCount: t.rowCount }));
|
|
222
230
|
return c.json(ok(tables));
|
|
223
231
|
} catch (e) {
|
|
@@ -48,7 +48,7 @@ class PostgresService {
|
|
|
48
48
|
const sslOpts = sslmode === "no-verify" || sslmode === "require"
|
|
49
49
|
? { rejectUnauthorized: false }
|
|
50
50
|
: sslmode === "disable" ? false : undefined;
|
|
51
|
-
const sql = postgres(connectionString, { max: 3, idle_timeout: 60, ssl: sslOpts as any });
|
|
51
|
+
const sql = postgres(connectionString, { max: 3, idle_timeout: 60, connect_timeout: 10, ssl: sslOpts as any });
|
|
52
52
|
const timer = setTimeout(() => this.disconnect(connectionString), IDLE_TIMEOUT_MS);
|
|
53
53
|
this.cache.set(connectionString, { sql, timer });
|
|
54
54
|
return sql;
|
|
@@ -806,12 +806,17 @@ export async function runSupervisor(opts: {
|
|
|
806
806
|
log("ERROR", `Unhandled rejection: ${reason}`);
|
|
807
807
|
});
|
|
808
808
|
|
|
809
|
-
// Full write to clear
|
|
809
|
+
// Full write to clear stale data — but preserve tunnel info during self-replace upgrade
|
|
810
|
+
// so the new supervisor can adopt the existing tunnel and keep the domain.
|
|
810
811
|
writeFileSync(PID_FILE(), String(process.pid));
|
|
812
|
+
const prevStatus = readStatus();
|
|
813
|
+
const isUpgrade = prevStatus.state === "upgrading";
|
|
811
814
|
writeStatus({
|
|
812
815
|
supervisorPid: process.pid, port: opts.port, host: opts.host, availableVersion: null,
|
|
813
816
|
state: "running", pausedAt: null, pauseReason: null, lastCrashError: null,
|
|
814
|
-
pid: null,
|
|
817
|
+
pid: null,
|
|
818
|
+
tunnelPid: isUpgrade ? (prevStatus.tunnelPid ?? null) : null,
|
|
819
|
+
shareUrl: isUpgrade ? (prevStatus.shareUrl ?? null) : null,
|
|
815
820
|
});
|
|
816
821
|
|
|
817
822
|
// Build __serve__ args
|
|
@@ -14,6 +14,7 @@ interface ColumnInfo {
|
|
|
14
14
|
interface ConnectionListProps {
|
|
15
15
|
connections: Connection[];
|
|
16
16
|
cachedTables: Map<number, CachedTable[]>;
|
|
17
|
+
refreshErrors?: Map<number, string>;
|
|
17
18
|
onOpenTable: (conn: Connection, tableName: string, schemaName: string) => void;
|
|
18
19
|
onRefreshTables: (id: number) => Promise<void>;
|
|
19
20
|
onEdit: (conn: Connection) => void;
|
|
@@ -27,7 +28,7 @@ interface GroupMap {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export function ConnectionList({
|
|
30
|
-
connections, cachedTables,
|
|
31
|
+
connections, cachedTables, refreshErrors,
|
|
31
32
|
onOpenTable, onRefreshTables, onEdit, onDelete,
|
|
32
33
|
onFetchColumns, columnCache,
|
|
33
34
|
}: ConnectionListProps) {
|
|
@@ -186,7 +187,11 @@ export function ConnectionList({
|
|
|
186
187
|
{isExpanded && (
|
|
187
188
|
<div className="ml-[11px] border-l border-dashed border-border pl-1">
|
|
188
189
|
{isRefreshing && tables.length === 0 && <p className="text-[10px] text-text-subtle px-2 py-1">Loading…</p>}
|
|
189
|
-
{!isRefreshing && tables.length === 0 &&
|
|
190
|
+
{!isRefreshing && tables.length === 0 && (
|
|
191
|
+
refreshErrors?.get(conn.id)
|
|
192
|
+
? <p className="text-[10px] text-red-500 px-2 py-1 break-all">{refreshErrors.get(conn.id)}</p>
|
|
193
|
+
: <p className="text-[10px] text-text-subtle px-2 py-1">No tables cached</p>
|
|
194
|
+
)}
|
|
190
195
|
{tables.length > 0 && (
|
|
191
196
|
<>
|
|
192
197
|
{tables.length > 5 && (
|
|
@@ -331,7 +331,7 @@ export function DataGrid({
|
|
|
331
331
|
if (pc !== col) stickyWidth = Math.max(stickyWidth, offset + (colWidths.get(pc) ?? 0));
|
|
332
332
|
}
|
|
333
333
|
const targetLeft = th.offsetLeft - stickyWidth;
|
|
334
|
-
container.scrollTo({ left: targetLeft, behavior: "
|
|
334
|
+
container.scrollTo({ left: targetLeft, behavior: "instant" });
|
|
335
335
|
setColSearchOpen(false);
|
|
336
336
|
}, [pinnedColOffsets, colWidths]);
|
|
337
337
|
|
|
@@ -7,7 +7,7 @@ import { ConnectionImportExport } from "./connection-import-export";
|
|
|
7
7
|
import { useConnections, type Connection, type CreateConnectionData, type UpdateConnectionData } from "./use-connections";
|
|
8
8
|
|
|
9
9
|
export function DatabaseSidebar() {
|
|
10
|
-
const { connections, loading, cachedTables, columnCache, createConnection, updateConnection, deleteConnection, testConnection, testRawConnection, refreshTables, fetchColumns, exportConnections, importConnections } = useConnections();
|
|
10
|
+
const { connections, loading, cachedTables, refreshErrors, columnCache, createConnection, updateConnection, deleteConnection, testConnection, testRawConnection, refreshTables, fetchColumns, exportConnections, importConnections } = useConnections();
|
|
11
11
|
const openTab = useTabStore((s) => s.openTab);
|
|
12
12
|
const [addOpen, setAddOpen] = useState(false);
|
|
13
13
|
const [editConn, setEditConn] = useState<Connection | null>(null);
|
|
@@ -62,6 +62,7 @@ export function DatabaseSidebar() {
|
|
|
62
62
|
<ConnectionList
|
|
63
63
|
connections={connections}
|
|
64
64
|
cachedTables={cachedTables}
|
|
65
|
+
refreshErrors={refreshErrors}
|
|
65
66
|
onOpenTable={handleOpenTable}
|
|
66
67
|
onRefreshTables={refreshTables}
|
|
67
68
|
onEdit={setEditConn}
|
|
@@ -41,6 +41,7 @@ export function useConnections() {
|
|
|
41
41
|
const [connections, setConnections] = useState<Connection[]>([]);
|
|
42
42
|
const [loading, setLoading] = useState(true);
|
|
43
43
|
const [cachedTables, setCachedTables] = useState<Map<number, CachedTable[]>>(new Map());
|
|
44
|
+
const [refreshErrors, setRefreshErrors] = useState<Map<number, string>>(new Map());
|
|
44
45
|
|
|
45
46
|
const fetchConnections = useCallback(async () => {
|
|
46
47
|
try {
|
|
@@ -84,15 +85,22 @@ export function useConnections() {
|
|
|
84
85
|
}, []);
|
|
85
86
|
|
|
86
87
|
const refreshTables = useCallback(async (id: number): Promise<void> => {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
88
|
+
setRefreshErrors((prev) => { const m = new Map(prev); m.delete(id); return m; });
|
|
89
|
+
try {
|
|
90
|
+
const raw = await api.get<{ name: string; schema: string; rowCount: number }[]>(`/api/db/connections/${id}/tables`);
|
|
91
|
+
const tables: CachedTable[] = raw.map((t) => ({
|
|
92
|
+
connectionId: id,
|
|
93
|
+
tableName: t.name,
|
|
94
|
+
schemaName: t.schema,
|
|
95
|
+
rowCount: t.rowCount,
|
|
96
|
+
cachedAt: new Date().toISOString(),
|
|
97
|
+
}));
|
|
98
|
+
setCachedTables((prev) => new Map(prev).set(id, tables));
|
|
99
|
+
} catch (e) {
|
|
100
|
+
const msg = (e as Error).message || "Connection failed";
|
|
101
|
+
setRefreshErrors((prev) => new Map(prev).set(id, msg));
|
|
102
|
+
throw e; // re-throw so callers know it failed
|
|
103
|
+
}
|
|
96
104
|
}, []);
|
|
97
105
|
|
|
98
106
|
/** Fetch column metadata for a table (lazy loaded for schema tree) */
|
|
@@ -117,8 +125,13 @@ export function useConnections() {
|
|
|
117
125
|
"/api/db/connections/import", data,
|
|
118
126
|
);
|
|
119
127
|
await fetchConnections();
|
|
128
|
+
// Auto-refresh table cache for newly imported connections (fire-and-forget)
|
|
129
|
+
const imported = result.connections ?? [];
|
|
130
|
+
if (imported.length > 0) {
|
|
131
|
+
Promise.all(imported.map((c) => refreshTables(c.id).catch(() => {})));
|
|
132
|
+
}
|
|
120
133
|
return result;
|
|
121
|
-
}, [fetchConnections]);
|
|
134
|
+
}, [fetchConnections, refreshTables]);
|
|
122
135
|
|
|
123
|
-
return { connections, loading, cachedTables, columnCache, createConnection, updateConnection, deleteConnection, testConnection, testRawConnection, refreshTables, fetchColumns, exportConnections, importConnections };
|
|
136
|
+
return { connections, loading, cachedTables, refreshErrors, columnCache, createConnection, updateConnection, deleteConnection, testConnection, testRawConnection, refreshTables, fetchColumns, exportConnections, importConnections };
|
|
124
137
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback } from "react";
|
|
2
2
|
import { api } from "@/lib/api-client";
|
|
3
3
|
import { toast } from "sonner";
|
|
4
|
-
import { Loader2, ArrowUpCircle, X } from "lucide-react";
|
|
4
|
+
import { Loader2, ArrowUpCircle, X, RefreshCw, CheckCircle2 } from "lucide-react";
|
|
5
5
|
|
|
6
6
|
const POLL_INTERVAL_MS = 60_000;
|
|
7
7
|
const DISMISS_KEY_PREFIX = "ppm-upgrade-dismissed-";
|
|
8
|
-
const RESTART_POLL_MS = 1_500;
|
|
9
|
-
const RESTART_TIMEOUT_MS = 60_000;
|
|
10
8
|
|
|
11
9
|
interface UpgradeStatus {
|
|
12
10
|
currentVersion: string;
|
|
@@ -21,28 +19,13 @@ interface UpgradeResult {
|
|
|
21
19
|
message?: string;
|
|
22
20
|
}
|
|
23
21
|
|
|
24
|
-
/**
|
|
25
|
-
async function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
while (Date.now() - start < RESTART_TIMEOUT_MS) {
|
|
30
|
-
await new Promise((r) => setTimeout(r, RESTART_POLL_MS));
|
|
31
|
-
try {
|
|
32
|
-
const res = await fetch("/api/health", { cache: "no-store" });
|
|
33
|
-
if (res.ok && serverWentDown) {
|
|
34
|
-
if ("caches" in window) {
|
|
35
|
-
const keys = await caches.keys();
|
|
36
|
-
await Promise.all(keys.map((k) => caches.delete(k)));
|
|
37
|
-
}
|
|
38
|
-
window.location.reload();
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
} catch {
|
|
42
|
-
serverWentDown = true;
|
|
43
|
-
}
|
|
22
|
+
/** Clear browser/SW caches and reload the page */
|
|
23
|
+
async function clearCachesAndReload() {
|
|
24
|
+
if ("caches" in window) {
|
|
25
|
+
const keys = await caches.keys();
|
|
26
|
+
await Promise.all(keys.map((k) => caches.delete(k)));
|
|
44
27
|
}
|
|
45
|
-
|
|
28
|
+
window.location.reload();
|
|
46
29
|
}
|
|
47
30
|
|
|
48
31
|
interface UpgradeBannerProps {
|
|
@@ -52,6 +35,7 @@ interface UpgradeBannerProps {
|
|
|
52
35
|
export function UpgradeBanner({ onVisibilityChange }: UpgradeBannerProps) {
|
|
53
36
|
const [availableVersion, setAvailableVersion] = useState<string | null>(null);
|
|
54
37
|
const [upgrading, setUpgrading] = useState(false);
|
|
38
|
+
const [upgradeComplete, setUpgradeComplete] = useState(false);
|
|
55
39
|
const [dismissed, setDismissed] = useState(false);
|
|
56
40
|
|
|
57
41
|
// Poll for upgrade status
|
|
@@ -86,12 +70,9 @@ export function UpgradeBanner({ onVisibilityChange }: UpgradeBannerProps) {
|
|
|
86
70
|
const data = await api.post<UpgradeResult>("/api/upgrade/apply");
|
|
87
71
|
|
|
88
72
|
if (data.restart) {
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
toast.warning("Upgrade installed but server hasn't restarted. Try refreshing manually.");
|
|
93
|
-
setUpgrading(false);
|
|
94
|
-
}
|
|
73
|
+
// Upgrade installed, server will restart — ask user to reload
|
|
74
|
+
setUpgrading(false);
|
|
75
|
+
setUpgradeComplete(true);
|
|
95
76
|
} else {
|
|
96
77
|
// No supervisor — manual restart needed
|
|
97
78
|
toast.info(data.message || "Upgrade installed. Restart PPM manually.");
|
|
@@ -102,10 +83,21 @@ export function UpgradeBanner({ onVisibilityChange }: UpgradeBannerProps) {
|
|
|
102
83
|
setDismissed(true);
|
|
103
84
|
}
|
|
104
85
|
} catch (e) {
|
|
105
|
-
|
|
106
|
-
|
|
86
|
+
// If fetch failed with a network error, the server likely died mid-response
|
|
87
|
+
// after the install succeeded (supervisor killed the server before response flushed).
|
|
88
|
+
// Show reload prompt instead of a confusing error.
|
|
89
|
+
const isNetworkError = e instanceof TypeError
|
|
90
|
+
|| (e as Error).message?.includes("fetch")
|
|
91
|
+
|| (e as Error).message?.includes("network");
|
|
92
|
+
if (isNetworkError) {
|
|
93
|
+
setUpgrading(false);
|
|
94
|
+
setUpgradeComplete(true);
|
|
95
|
+
} else {
|
|
96
|
+
toast.error(`Upgrade failed: ${(e as Error).message}`);
|
|
97
|
+
setUpgrading(false);
|
|
98
|
+
}
|
|
107
99
|
}
|
|
108
|
-
}, []);
|
|
100
|
+
}, [availableVersion]);
|
|
109
101
|
|
|
110
102
|
const handleDismiss = useCallback(() => {
|
|
111
103
|
if (availableVersion) {
|
|
@@ -114,7 +106,7 @@ export function UpgradeBanner({ onVisibilityChange }: UpgradeBannerProps) {
|
|
|
114
106
|
setDismissed(true);
|
|
115
107
|
}, [availableVersion]);
|
|
116
108
|
|
|
117
|
-
const visible = !!availableVersion && !dismissed;
|
|
109
|
+
const visible = (!!availableVersion && !dismissed) || upgradeComplete;
|
|
118
110
|
|
|
119
111
|
useEffect(() => {
|
|
120
112
|
onVisibilityChange?.(visible);
|
|
@@ -123,12 +115,30 @@ export function UpgradeBanner({ onVisibilityChange }: UpgradeBannerProps) {
|
|
|
123
115
|
if (!visible) return null;
|
|
124
116
|
|
|
125
117
|
return (
|
|
126
|
-
<div className=
|
|
127
|
-
|
|
118
|
+
<div className={`w-full text-white px-3 py-1 flex items-center justify-between gap-2 z-50 text-sm shrink-0 ${
|
|
119
|
+
upgradeComplete ? "bg-green-600 dark:bg-green-700" : "bg-blue-600 dark:bg-blue-700"
|
|
120
|
+
}`}>
|
|
121
|
+
{upgradeComplete ? (
|
|
122
|
+
<>
|
|
123
|
+
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
124
|
+
<CheckCircle2 className="size-4 shrink-0" />
|
|
125
|
+
<span className="truncate">
|
|
126
|
+
Upgrade to v{availableVersion} installed! Reload to apply.
|
|
127
|
+
</span>
|
|
128
|
+
</div>
|
|
129
|
+
<button
|
|
130
|
+
onClick={clearCachesAndReload}
|
|
131
|
+
className="bg-white text-green-600 font-medium rounded-full px-3 py-0.5 text-xs min-h-[28px] min-w-[28px] flex items-center gap-1.5 justify-center hover:bg-green-50 active:bg-green-100 transition-colors shrink-0"
|
|
132
|
+
>
|
|
133
|
+
<RefreshCw className="size-3" />
|
|
134
|
+
Reload
|
|
135
|
+
</button>
|
|
136
|
+
</>
|
|
137
|
+
) : upgrading ? (
|
|
128
138
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
129
139
|
<Loader2 className="size-4 animate-spin shrink-0" />
|
|
130
140
|
<span className="truncate">
|
|
131
|
-
Upgrading to v{availableVersion}...
|
|
141
|
+
Upgrading to v{availableVersion}...
|
|
132
142
|
</span>
|
|
133
143
|
</div>
|
|
134
144
|
) : (
|
|
@@ -75,7 +75,12 @@ class ApiClient {
|
|
|
75
75
|
throw new Error("Unauthorized");
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
let json: any;
|
|
79
|
+
try {
|
|
80
|
+
json = await res.json();
|
|
81
|
+
} catch {
|
|
82
|
+
throw new Error(res.ok ? "Empty response from server" : `Server error (HTTP ${res.status})`);
|
|
83
|
+
}
|
|
79
84
|
|
|
80
85
|
if (json.ok === false) {
|
|
81
86
|
throw new Error(json.error ?? `HTTP ${res.status}`);
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{o as e}from"./rolldown-runtime-FhOqtrmT.js";import{b as t,x as n}from"./vendor-markdown-0Mxgxy0L.js";import{t as r}from"./createLucideIcon-BjHrJDVb.js";import{n as i,r as a,t as o}from"./plus-51UQ45rf.js";import{t as s}from"./chevron-right-BzAdxJRG.js";import{t as c}from"./database-D4DIhgi-.js";import{n as l,r as u,t as d}from"./x-DlFGzN8d.js";import{t as f}from"./refresh-cw-LlbZDJpO.js";import{t as p}from"./trash-2-CJYoLw7Q.js";import{t as m}from"./api-client-Bn-Pi9k5.js";import"./vendor-mermaid-BlWh9BJO.js";import"./settings-store-B9axDbuA.js";import{t as h}from"./tab-store-B3M9hjho.js";import{E as g,K as _,N as v,O as y,P as b,h as x,k as S}from"./index-Bl3W1FYm.js";import"./use-monaco-theme-BERjR8IA.js";import{t as C}from"./sql-query-editor-BnpKNGG0.js";import{n as w}from"./csv-parser--2WJNgS7.js";var T=r(`columns-3`,[[`rect`,{width:`18`,height:`18`,x:`3`,y:`3`,rx:`2`,key:`afitv7`}],[`path`,{d:`M9 3v18`,key:`fh3hqa`}],[`path`,{d:`M15 3v18`,key:`14nvp0`}]]),E=r(`funnel`,[[`path`,{d:`M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z`,key:`sc7q7i`}]]),D=e(n(),1);function O(e,t,n,r){return`ppm-db-${e}-${n}.${t}-p${r}`}function ee(e,t,n,r){try{let i=sessionStorage.getItem(O(e,t,n,r));return i?JSON.parse(i):null}catch{return null}}function te(e,t,n,r,i,a){try{sessionStorage.setItem(O(e,t,n,r),JSON.stringify({data:i,cols:a}))}catch{}}function ne(e){let t=`/api/db/connections/${e}`,[n,r]=(0,D.useState)(null),[i,a]=(0,D.useState)(`public`),[o,s]=(0,D.useState)(null),[c,l]=(0,D.useState)([]),[u,d]=(0,D.useState)(!1),[f,p]=(0,D.useState)(null),[h,g]=(0,D.useState)(1),[_,v]=(0,D.useState)(null),[y,b]=(0,D.useState)(null),[x,S]=(0,D.useState)(!1),[C,w]=(0,D.useState)(null),[T,E]=(0,D.useState)(`ASC`),O=(0,D.useCallback)(async(r,a,o,c,u)=>{let f=r??n,g=a??i;if(!f)return;d(!0);let _=c===void 0?C:c,v=u??T;try{let n=_?`&orderBy=${encodeURIComponent(_)}&orderDir=${v}`:``,[r,i]=await Promise.all([m.get(`${t}/data?table=${encodeURIComponent(f)}&schema=${g}&page=${o??h}&limit=100${n}`),m.get(`${t}/schema?table=${encodeURIComponent(f)}&schema=${g}`)]);s(r),l(i),te(e,f,g,o??h,r,i)}catch(e){p(e.message)}finally{d(!1)}},[t,e,n,i,h,C,T]),ne=(0,D.useCallback)((t,n=`public`)=>{r(t),a(n),g(1),v(null);let i=ee(e,t,n,1);i?(s(i.data),l(i.cols),d(!1),O(t,n,1)):O(t,n,1)},[e,O]),k=(0,D.useCallback)(e=>{g(e),O(void 0,void 0,e)},[O]),A=(0,D.useCallback)(async e=>{S(!0),b(null);try{let r=await m.post(`${t}/query`,{sql:e});v(r),r.changeType===`modify`&&O(n??void 0,i)}catch(e){b(e.message)}finally{S(!1)}},[t,n,i,O]),re=(0,D.useCallback)(async(e,r,a,o)=>{if(!n)return;let s=n,c=i;try{await m.put(`${t}/cell`,{table:s,schema:c,pkColumn:e,pkValue:r,column:a,value:o}),O(s,c)}catch(e){p(e.message)}},[t,n,i,O]),j=(0,D.useCallback)(async(e,r)=>{if(!n)return;let a=n,o=i;try{await m.del(`${t}/row`,{table:a,schema:o,pkColumn:e,pkValue:r}),O(a,o)}catch(e){p(e.message)}},[t,n,i,O]);return{selectedTable:n,selectedSchema:i,selectTable:ne,tableData:o,schema:c,loading:u,error:f,page:h,setPage:k,orderBy:C,orderDir:T,toggleSort:(0,D.useCallback)(e=>{let t,n=`ASC`;C===e?T===`ASC`?(t=e,n=`DESC`):(t=null,n=`ASC`):(t=e,n=`ASC`),w(t),E(n),g(1),O(void 0,void 0,1,t,n)},[C,T,O]),queryResult:_,queryError:y,queryLoading:x,executeQuery:A,updateCell:re,deleteRow:j,bulkDelete:(0,D.useCallback)(async(e,r)=>{if(!n)return;let a=n,o=i;try{await m.post(`${t}/rows/delete`,{table:a,schema:o,pkColumn:e,pkValues:r}),O(a,o)}catch(e){p(e.message)}},[t,n,i,O]),insertRow:(0,D.useCallback)(async e=>{if(!n)return;let r=n,a=i;try{await m.post(`${t}/row`,{table:r,schema:a,values:e}),O(r,a)}catch(e){throw p(e.message),e}},[t,n,i,O]),refreshData:O,queryAsTable:(0,D.useCallback)(async e=>{d(!0);try{let n=await m.post(`${t}/query`,{sql:e});n.changeType===`select`&&s({columns:n.columns,rows:n.rows,total:n.rows.length,page:1,limit:n.rows.length})}catch(e){p(e.message)}finally{d(!1)}},[t])}}var k=t();function A(e,t,n){let r=new Blob([t],{type:n}),i=URL.createObjectURL(r),a=document.createElement(`a`);a.href=i,a.download=e,a.click(),URL.revokeObjectURL(i)}function re({columns:e,rows:t,filename:n=`export`,exportAllUrl:r}){let[i,a]=(0,D.useState)(!1),[o,s]=(0,D.useState)(!1),c=(0,D.useRef)(null);(0,D.useEffect)(()=>{if(!i)return;let e=e=>{c.current&&!c.current.contains(e.target)&&a(!1)};return document.addEventListener(`mousedown`,e),()=>document.removeEventListener(`mousedown`,e)},[i]);let l=()=>{let r=w(e,t.map(t=>e.map(e=>String(t[e]??``))));A(`${n}.csv`,r,`text/csv`),a(!1)},d=()=>{let e=JSON.stringify(t,null,2);A(`${n}.json`,e,`application/json`),a(!1)},f=async e=>{if(r){s(!0);try{let t=await(await fetch(`${r}&format=${e}&limit=10000`)).text(),i=e===`csv`?`text/csv`:`application/json`;A(`${n}-all.${e}`,t,i)}catch{}s(!1),a(!1)}};return e.length===0||t.length===0?null:(0,k.jsxs)(`div`,{className:`relative`,ref:c,children:[(0,k.jsx)(`button`,{type:`button`,onClick:()=>a(e=>!e),className:`p-1 rounded text-muted-foreground hover:text-foreground transition-colors`,title:`Export`,children:(0,k.jsx)(u,{className:`size-3.5`})}),i&&(0,k.jsxs)(`div`,{className:`absolute right-0 top-full mt-1 z-50 bg-popover border border-border rounded-md shadow-md py-1 min-w-[160px] text-xs`,children:[(0,k.jsx)(`button`,{onClick:l,className:`w-full text-left px-3 py-1.5 hover:bg-muted transition-colors`,children:`Export Page (CSV)`}),(0,k.jsx)(`button`,{onClick:d,className:`w-full text-left px-3 py-1.5 hover:bg-muted transition-colors`,children:`Export Page (JSON)`}),r&&(0,k.jsxs)(k.Fragment,{children:[(0,k.jsx)(`div`,{className:`border-t border-border my-1`}),(0,k.jsx)(`button`,{onClick:()=>f(`csv`),disabled:o,className:`w-full text-left px-3 py-1.5 hover:bg-muted transition-colors disabled:opacity-50`,children:o?`Exporting…`:`Export All (CSV)`}),(0,k.jsx)(`button`,{onClick:()=>f(`json`),disabled:o,className:`w-full text-left px-3 py-1.5 hover:bg-muted transition-colors disabled:opacity-50`,children:o?`Exporting…`:`Export All (JSON)`})]})]})]})}function j({tableData:e,schema:t,loading:n,page:r,onPageChange:c,onCellUpdate:l,onRowDelete:u,orderBy:f,orderDir:m,onToggleSort:b,onBulkDelete:C,onInsertRow:w,connectionId:O,selectedTable:ee,selectedSchema:te,connectionName:ne,columnFilters:A={},onColumnFilter:j}){let[M,N]=(0,D.useState)(null),[P,F]=(0,D.useState)(``),[I,L]=(0,D.useState)(null),[R,se]=(0,D.useState)(``),[z,B]=(0,D.useState)(new Set),[ce,le]=(0,D.useState)(!1),[ue,de]=(0,D.useState)({}),[fe,V]=(0,D.useState)(null),[pe,me]=(0,D.useState)(!1),{openTab:H}=h(x(e=>({openTab:e.openTab}))),he=(0,D.useCallback)(e=>{H({type:`editor`,title:e.col,projectId:null,closable:!0,metadata:{inlineContent:e.value,inlineLanguage:ie(e.value)}})},[H]),[U,ge]=(0,D.useState)(new Set),[_e,ve]=(0,D.useState)(new Set),[ye,be]=(0,D.useState)(null),[xe,W]=(0,D.useState)(!1),Se=(0,D.useRef)(null),G=(0,D.useMemo)(()=>t.find(e=>e.pk)?.name||(t.find(e=>e.name.toLowerCase()===`id`)?.name??null),[t]),Ce=(0,D.useCallback)(e=>{let t=JSON.stringify(e,null,2),n=G?String(e[G]??``):``;H({type:`editor`,title:n?`Row ${n}`:`Row`,projectId:null,closable:!0,metadata:{inlineContent:t,inlineLanguage:`json`}})},[H,G]),we=(0,D.useRef)(M);we.current=M;let K=(0,D.useRef)(P);K.current=P;let Te=(0,D.useRef)(z);Te.current=z;let Ee=(0,D.useRef)(I);Ee.current=I;let De=(0,D.useCallback)((e,t,n)=>{N({rowIdx:e,col:t}),F(n==null?``:typeof n==`object`?JSON.stringify(n):String(n))},[]),Oe=(0,D.useCallback)(()=>{let t=we.current;if(!t||!e||!G)return;let n=e.rows[t.rowIdx];if(!n)return;let r=n[t.col];String(r??``)!==K.current&&l(G,n[G],t.col,K.current===``?null:K.current),N(null)},[e,G,l]),ke=(0,D.useCallback)(()=>N(null),[]),Ae=(0,D.useCallback)(t=>{if(!e||!G||!u)return;let n=e.rows[t];n&&(u(G,n[G]),L(null))},[e,G,u]),je=(0,D.useCallback)(e=>{ge(t=>{let n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n})},[]),Me=(0,D.useCallback)(e=>{ve(t=>{let n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n})},[]),Ne=(0,D.useCallback)((e,t)=>{let n={...A};t?n[e]=t:delete n[e],j?.(n)},[A,j]),Pe=(0,D.useCallback)(e=>{B(t=>{let n=new Set(t);return n.has(e)?n.delete(e):n.add(e),n})},[]),Fe=(0,D.useCallback)(()=>{e&&B(t=>t.size===e.rows.length?new Set:new Set(e.rows.map((e,t)=>t)))},[e]),Ie=(0,D.useCallback)(()=>{!e||!G||!C||(C(G,Array.from(z).map(t=>e.rows[t]?.[G]).filter(e=>e!=null)),B(new Set),me(!1))},[e,G,C,z]),Le=(0,D.useCallback)(async()=>{if(w){V(null);try{let e={};for(let[t,n]of Object.entries(ue))n!==``&&(e[t]=n);await w(e),le(!1),de({})}catch(e){V(e.message)}}},[w,ue]),q=(0,D.useMemo)(()=>{if(!e||!R)return e?.rows??[];let t=R.toLowerCase();return e.rows.filter(n=>e.columns.some(e=>String(n[e]??``).toLowerCase().includes(t)))},[e,R]),Re=(0,D.useRef)(null);(0,D.useEffect)(()=>{let t=Re.current;if(!t)return;let n=t=>{if(t.key===`Escape`){W(!1);return}let n=t.target?.tagName;if(!(n===`INPUT`||n===`TEXTAREA`)){if(t.key===`/`){t.preventDefault(),W(!0);return}if(!(!(t.metaKey||t.ctrlKey)||!e)&&(t.key===`a`&&(t.preventDefault(),B(new Set(e.rows.map((e,t)=>t)))),t.key===`c`&&z.size>0)){t.preventDefault();let n=e.columns,r=n.join(` `),i=Array.from(z).sort((e,t)=>e-t).map(t=>n.map(n=>{let r=e.rows[t]?.[n];return r==null?``:typeof r==`object`?JSON.stringify(r):String(r)}).join(` `));navigator.clipboard.writeText([r,...i].join(`
|
|
2
|
-
`))}}};return t.addEventListener(`keydown`,n),()=>t.removeEventListener(`keydown`,n)},[e,z]);let J=(0,D.useMemo)(()=>{if(!e)return[];let t=e.columns.filter(e=>U.has(e)),n=e.columns.filter(e=>!U.has(e));return[...t,...n]},[e?.columns,U]),ze=(0,D.useRef)(null),[Be,Ve]=(0,D.useState)(0),[Y,He]=(0,D.useState)(new Map);(0,D.useEffect)(()=>{let e=ze.current;if(!e)return;let t=()=>{Ve(e.offsetHeight);let t=new Map;e.querySelectorAll(`th[data-col]`).forEach(e=>{t.set(e.dataset.col,e.offsetWidth)});let n=e.querySelector(`th[data-col='_cb']`);n&&t.set(`_cb`,n.offsetWidth),He(t)};t();let n=new ResizeObserver(t);return n.observe(e),()=>n.disconnect()},[e?.columns,U]);let X=(0,D.useMemo)(()=>{let e=new Map,t=Y.get(`_cb`)??(G?40:0);for(let n of J){if(!U.has(n))break;e.set(n,t),t+=Y.get(n)??100}return e},[J,U,G,Y]),Z=(0,D.useMemo)(()=>Array.from(_e).sort((e,t)=>e-t).map(e=>({idx:e,row:q[e]})).filter(e=>e.row),[_e,q]),Q=(0,D.useRef)(new Map),[$,Ue]=(0,D.useState)(new Map),We=(0,D.useCallback)((e,t)=>{t?Q.current.set(e,t):Q.current.delete(e)},[]);(0,D.useEffect)(()=>{if(Z.length===0){$.size>0&&Ue(new Map);return}let e=requestAnimationFrame(()=>{let e=new Map;for(let{idx:t}of Z){let n=Q.current.get(t);n&&e.set(t,n.offsetHeight)}Ue(e)});return()=>cancelAnimationFrame(e)},[Z,e]);let Ge=(0,D.useMemo)(()=>{let e=new Map,t=Be;for(let{idx:n}of Z)e.set(n,t),t+=$.get(n)??28;return e},[Be,Z,$]),Ke=(0,D.useCallback)(e=>{let t=Se.current,n=t?.querySelector(`th[data-col="${e}"]`);if(!t||!n)return;let r=0,i=t.querySelector(`th[data-col="_cb"]`);i&&(r+=i.offsetWidth);for(let[t,n]of X)t!==e&&(r=Math.max(r,n+(Y.get(t)??0)));let a=n.offsetLeft-r;t.scrollTo({left:a,behavior:`smooth`}),W(!1)},[X,Y]);if(!e)return(0,k.jsx)(`div`,{className:`flex items-center justify-center h-full text-xs text-muted-foreground`,children:n?(0,k.jsx)(v,{className:`size-4 animate-spin`}):`Select a table`});let qe=Math.ceil(e.total/e.limit)||1,Je=z.size>0,Ye=z.size===e.rows.length&&e.rows.length>0;return(0,k.jsxs)(`div`,{ref:Re,tabIndex:0,className:`flex flex-col h-full overflow-hidden outline-none`,children:[(0,k.jsxs)(`div`,{className:`flex items-center gap-2 px-2 py-1 border-b border-border bg-background shrink-0`,children:[(0,k.jsxs)(`div`,{className:`flex items-center gap-1 flex-1`,children:[(0,k.jsx)(g,{className:`size-3 text-muted-foreground`}),(0,k.jsx)(`input`,{type:`text`,value:R,onChange:e=>se(e.target.value),placeholder:`Search current page…`,className:`flex-1 text-xs bg-transparent outline-none text-foreground placeholder:text-muted-foreground`}),R&&(0,k.jsx)(`button`,{type:`button`,onClick:()=>se(``),className:`text-muted-foreground hover:text-foreground`,children:(0,k.jsx)(d,{className:`size-3`})})]}),(0,k.jsxs)(`div`,{className:`relative`,children:[(0,k.jsx)(`button`,{type:`button`,onClick:()=>W(!xe),className:`p-0.5 rounded transition-colors ${xe?`text-primary`:`text-muted-foreground hover:text-foreground`}`,title:`Jump to column ( / )`,children:(0,k.jsx)(T,{className:`size-3.5`})}),xe&&(0,k.jsx)(ae,{columns:e.columns,onSelect:Ke,onClose:()=>W(!1)})]}),Je&&(0,k.jsxs)(`div`,{className:`flex items-center gap-1.5 text-xs`,children:[(0,k.jsxs)(`span`,{className:`text-muted-foreground`,children:[z.size,` selected`]}),C&&G&&(pe?(0,k.jsxs)(`span`,{className:`flex items-center gap-1`,children:[(0,k.jsxs)(`button`,{type:`button`,onClick:Ie,className:`text-destructive text-[10px] font-medium hover:underline`,children:[`Delete `,z.size,`?`]}),(0,k.jsx)(`button`,{type:`button`,onClick:()=>me(!1),className:`text-muted-foreground text-[10px] hover:underline`,children:`Cancel`})]}):(0,k.jsx)(`button`,{type:`button`,onClick:()=>me(!0),className:`p-0.5 text-muted-foreground hover:text-destructive`,children:(0,k.jsx)(p,{className:`size-3`})})),(0,k.jsx)(re,{columns:e.columns,rows:Array.from(z).map(t=>e.rows[t]).filter(Boolean),filename:`${ne??`db`}-selected`})]}),w&&(0,k.jsx)(`button`,{type:`button`,onClick:()=>{le(!0),de({}),V(null)},className:`p-0.5 rounded text-muted-foreground hover:text-primary transition-colors`,title:`Insert row`,children:(0,k.jsx)(o,{className:`size-3.5`})})]}),ce&&(0,k.jsxs)(`div`,{className:`px-2 py-1.5 border-b border-border bg-muted/30 text-xs space-y-1`,children:[(0,k.jsx)(`div`,{className:`flex flex-wrap gap-1.5`,children:t.filter(e=>!e.pk).map(e=>(0,k.jsxs)(`div`,{className:`flex items-center gap-1`,children:[(0,k.jsx)(`label`,{className:`text-muted-foreground text-[10px] w-16 truncate`,title:e.name,children:e.name}),(0,k.jsx)(`input`,{value:ue[e.name]??``,onChange:t=>de(n=>({...n,[e.name]:t.target.value})),placeholder:e.defaultValue??(e.nullable?`NULL`:``),className:`h-5 w-24 text-[11px] px-1 rounded border border-border bg-background outline-none focus:border-primary`})]},e.name))}),(0,k.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,k.jsx)(`button`,{type:`button`,onClick:Le,className:`text-[10px] px-2 py-0.5 rounded bg-primary text-primary-foreground hover:bg-primary/90`,children:`Save`}),(0,k.jsx)(`button`,{type:`button`,onClick:()=>le(!1),className:`text-[10px] text-muted-foreground hover:underline`,children:`Cancel`}),fe&&(0,k.jsx)(`span`,{className:`text-[10px] text-destructive`,children:fe})]})]}),(0,k.jsxs)(`div`,{ref:Se,className:`flex-1 overflow-auto relative`,children:[n&&(0,k.jsx)(`div`,{className:`absolute inset-0 z-30 flex items-center justify-center bg-background/60`,children:(0,k.jsx)(v,{className:`size-5 animate-spin text-primary`})}),(0,k.jsxs)(`table`,{className:`w-full text-xs`,style:{borderCollapse:`separate`,borderSpacing:0},children:[(0,k.jsx)(`thead`,{ref:ze,className:`sticky top-0 z-20 bg-muted`,children:(0,k.jsxs)(`tr`,{children:[G&&(0,k.jsx)(`th`,{"data-col":`_cb`,className:`px-2 py-1.5 text-left font-medium text-muted-foreground border-b border-border w-10 bg-muted`,style:{position:`sticky`,left:0,zIndex:30},children:(0,k.jsx)(`input`,{type:`checkbox`,checked:Ye,onChange:Fe,className:`size-3 accent-primary`})}),J.map(e=>{let n=t.find(t=>t.name===e)?.pk,r=f===e,o=U.has(e),s=!!A[e],c=ye===e,l=X.get(e);return(0,k.jsxs)(`th`,{"data-col":e,className:`group/th px-2 py-1.5 text-left font-medium text-muted-foreground border-b border-border whitespace-nowrap bg-muted ${o?`border-r border-r-primary/20`:``}`,style:l==null?void 0:{position:`sticky`,left:l,zIndex:25},children:[(0,k.jsxs)(`div`,{className:`flex items-center gap-0.5`,children:[(0,k.jsxs)(`button`,{type:`button`,onClick:()=>b(e),className:`flex items-center gap-0.5 ${n?`font-bold`:``} hover:text-foreground transition-colors`,children:[e,r&&m===`ASC`&&(0,k.jsx)(i,{className:`size-3`}),r&&m===`DESC`&&(0,k.jsx)(a,{className:`size-3`})]}),j&&(0,k.jsx)(`button`,{type:`button`,title:`Filter column`,onClick:()=>be(c?null:e),className:`p-0.5 rounded transition-colors ${s||c?`text-primary`:`text-muted-foreground/40 md:opacity-0 md:group-hover/th:opacity-100 hover:text-foreground`}`,children:(0,k.jsx)(E,{className:`size-2.5`})}),(0,k.jsx)(`button`,{type:`button`,title:o?`Unpin column`:`Pin column`,onClick:()=>je(e),className:`p-0.5 rounded transition-colors ${o?`text-primary`:`text-muted-foreground/40 md:opacity-0 md:group-hover/th:opacity-100 hover:text-foreground`}`,children:o?(0,k.jsx)(S,{className:`size-2.5`}):(0,k.jsx)(y,{className:`size-2.5`})})]}),c&&(0,k.jsxs)(`div`,{className:`mt-1 flex items-center gap-1`,children:[(0,k.jsx)(`input`,{autoFocus:!0,type:`text`,value:A[e]??``,placeholder:`ILIKE filter…`,onChange:t=>Ne(e,t.target.value),onKeyDown:e=>{e.key===`Escape`&&be(null)},className:`w-full h-5 text-[10px] px-1 rounded border border-border bg-background outline-none focus:border-primary`}),s&&(0,k.jsx)(`button`,{type:`button`,onClick:()=>Ne(e,``),className:`text-muted-foreground hover:text-foreground`,children:(0,k.jsx)(d,{className:`size-2.5`})})]})]},e)}),u&&G&&(0,k.jsx)(`th`,{className:`px-2 py-1.5 border-b border-border w-14 bg-muted`})]})}),(0,k.jsxs)(`tbody`,{children:[Z.map(({idx:e,row:t})=>(0,k.jsx)(oe,{row:t,rowIdx:e,columns:J,selected:z.has(e),onToggleSelect:Pe,pkCol:G,editingCell:M,editValue:P,onStartEdit:De,onCommitEdit:Oe,onCancelEdit:ke,onSetEditValue:F,showDelete:!!u,confirmingDelete:I===e,onDelete:Ae,onConfirmDelete:L,onViewCell:he,onViewRow:Ce,pinned:!0,onTogglePin:Me,pinnedCols:U,pinnedColOffsets:X,stickyTop:Ge.get(e)??Be,trRef:t=>We(e,t)},`pin-${e}`)),q.map((e,t)=>_e.has(t)?null:(0,k.jsx)(oe,{row:e,rowIdx:t,columns:J,selected:z.has(t),onToggleSelect:Pe,pkCol:G,editingCell:M,editValue:P,onStartEdit:De,onCommitEdit:Oe,onCancelEdit:ke,onSetEditValue:F,showDelete:!!u,confirmingDelete:I===t,onDelete:Ae,onConfirmDelete:L,onViewCell:he,onViewRow:Ce,pinned:!1,onTogglePin:Me,pinnedCols:U,pinnedColOffsets:X},t)),q.length===0&&(0,k.jsx)(`tr`,{children:(0,k.jsx)(`td`,{colSpan:J.length+2,className:`px-2 py-8 text-center text-muted-foreground`,children:`No data`})})]})]})]}),(0,k.jsxs)(`div`,{className:`flex items-center justify-between px-3 py-1.5 border-t border-border bg-background shrink-0 text-xs text-muted-foreground`,children:[(0,k.jsxs)(`span`,{children:[e.total.toLocaleString(),` rows`]}),(0,k.jsxs)(`div`,{className:`hidden md:flex items-center gap-2 text-[10px] text-muted-foreground/50`,children:[(0,k.jsxs)(`span`,{children:[(0,k.jsx)(`kbd`,{className:`px-1 py-0.5 rounded bg-muted text-[9px]`,children:`/`}),` columns`]}),(0,k.jsxs)(`span`,{children:[(0,k.jsxs)(`kbd`,{className:`px-1 py-0.5 rounded bg-muted text-[9px]`,children:[`⌘`,`A`]}),` select all`]}),(0,k.jsxs)(`span`,{children:[(0,k.jsxs)(`kbd`,{className:`px-1 py-0.5 rounded bg-muted text-[9px]`,children:[`⌘`,`C`]}),` copy`]})]}),(0,k.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,k.jsx)(`button`,{type:`button`,disabled:r<=1,onClick:()=>c(r-1),className:`p-0.5 rounded hover:bg-muted disabled:opacity-30`,children:(0,k.jsx)(_,{className:`size-3.5`})}),(0,k.jsxs)(`span`,{children:[r,` / `,qe]}),(0,k.jsx)(`button`,{type:`button`,disabled:r>=qe,onClick:()=>c(r+1),className:`p-0.5 rounded hover:bg-muted disabled:opacity-30`,children:(0,k.jsx)(s,{className:`size-3.5`})})]})]})]})}function M(e){return e==null?`NULL`:typeof e==`object`?JSON.stringify(e):String(e)}var N=200;function P(e){if(e==null)return!1;if(typeof e==`object`)return!0;let t=String(e);if(t.length>=N)return!0;let n=t.trimStart();return!!((n[0]===`{`||n[0]===`[`)&&(n.endsWith(`}`)||n.endsWith(`]`))||n.startsWith(`<?xml`)||n.startsWith(`<`)&&n.endsWith(`>`))}function ie(e){let t=e.trimStart();if(t[0]===`{`||t[0]===`[`)try{return JSON.parse(t),`json`}catch{}return t.startsWith(`<?xml`)||t.startsWith(`<`)&&/<\/\w+>/.test(t)?`xml`:t.startsWith(`---`)||/^\w+:\s/m.test(t)?`yaml`:`plaintext`}function ae({columns:e,onSelect:t,onClose:n}){let[r,i]=(0,D.useState)(``),[a,o]=(0,D.useState)(0),s=(0,D.useMemo)(()=>{if(!r)return e;let t=r.toLowerCase();return e.filter(e=>e.toLowerCase().includes(t))},[e,r]),c=(0,D.useRef)(null);return(0,D.useEffect)(()=>{c.current?.scrollIntoView({block:`nearest`})},[a]),(0,k.jsxs)(`div`,{className:`absolute top-full right-0 mt-1 z-50 w-52 max-h-56 bg-popover border border-border rounded-md shadow-lg overflow-hidden flex flex-col`,children:[(0,k.jsx)(`input`,{autoFocus:!0,type:`text`,value:r,onChange:e=>{i(e.target.value),o(0)},onKeyDown:e=>{e.key===`Escape`?n():e.key===`ArrowDown`?(e.preventDefault(),o(e=>Math.min(e+1,s.length-1))):e.key===`ArrowUp`?(e.preventDefault(),o(e=>Math.max(e-1,0))):e.key===`Enter`&&s[a]&&t(s[a])},placeholder:`Search columns…`,className:`px-2 py-1.5 text-xs bg-transparent outline-none border-b border-border text-foreground placeholder:text-muted-foreground`}),(0,k.jsx)(`div`,{className:`overflow-auto flex-1`,children:s.map((e,n)=>(0,k.jsx)(`button`,{type:`button`,onClick:()=>t(e),ref:n===a?c:void 0,className:`w-full text-left px-2 py-1 text-xs truncate ${n===a?`bg-primary/10 text-primary`:`text-foreground hover:bg-muted`}`,children:e},e))})]})}var oe=(0,D.memo)(function({row:e,rowIdx:t,columns:n,selected:r,onToggleSelect:i,pkCol:a,editingCell:o,editValue:s,onStartEdit:c,onCommitEdit:u,onCancelEdit:d,onSetEditValue:f,showDelete:m,confirmingDelete:h,onDelete:g,onConfirmDelete:_,onViewCell:v,onViewRow:b,pinned:x,onTogglePin:C,pinnedCols:w,pinnedColOffsets:T,stickyTop:E,trRef:D}){let O=x?`bg-muted`:r?`bg-primary/5`:`bg-background`;return(0,k.jsxs)(`tr`,{ref:D,className:`group ${x?``:`hover:bg-muted/30`}`,style:x?{position:`sticky`,top:E,zIndex:15}:void 0,children:[a&&(0,k.jsx)(`td`,{className:`px-2 py-1 border-b border-border/50 ${O}`,style:{position:`sticky`,left:0,zIndex:12},children:(0,k.jsxs)(`span`,{className:`flex items-center gap-0.5`,children:[(0,k.jsx)(`input`,{type:`checkbox`,checked:r,onChange:()=>i(t),className:`size-3 accent-primary`}),(0,k.jsx)(`button`,{type:`button`,title:`View row as JSON`,onClick:()=>b(e),className:`p-0.5 rounded transition-colors text-muted-foreground/30 md:opacity-0 md:group-hover:opacity-100 hover:text-foreground`,children:(0,k.jsx)(l,{className:`size-2.5`})}),(0,k.jsx)(`button`,{type:`button`,title:x?`Unpin row`:`Pin row`,onClick:()=>C(t),className:`p-0.5 rounded transition-colors ${x?`text-primary`:`text-muted-foreground/30 md:opacity-0 md:group-hover:opacity-100 hover:text-foreground`}`,children:x?(0,k.jsx)(S,{className:`size-2.5`}):(0,k.jsx)(y,{className:`size-2.5`})})]})}),n.map(n=>{let r=o?.rowIdx===t&&o?.col===n,i=e[n],p=!r&&P(i),m=w.has(n),h=T.get(n);return(0,k.jsx)(`td`,{className:`px-2 py-1 max-w-[300px] border-b border-border/50 ${m?`border-r border-r-primary/20`:``} ${m||x?O:``}`,style:h==null?void 0:{position:`sticky`,left:h,zIndex:10},children:r?(0,k.jsx)(`input`,{autoFocus:!0,className:`w-full bg-transparent border border-primary/50 rounded px-1 py-0 text-xs outline-none`,value:s,onChange:e=>f(e.target.value),onBlur:u,onKeyDown:e=>{e.key===`Enter`&&u(),e.key===`Escape`&&d()}}):(0,k.jsxs)(`span`,{className:`flex items-center gap-0.5`,children:[(0,k.jsx)(`span`,{className:`cursor-pointer truncate flex-1 ${i==null?`text-muted-foreground/40 italic`:``}`,onDoubleClick:()=>a&&c(t,n,i),title:M(i),children:M(i)}),p&&(0,k.jsx)(`button`,{type:`button`,title:`View full content`,onClick:()=>v({col:n,value:M(i)}),className:`shrink-0 p-0.5 rounded text-muted-foreground/50 hover:text-foreground transition-colors`,children:(0,k.jsx)(l,{className:`size-3`})})]})},n)}),m&&a&&(0,k.jsx)(`td`,{className:`px-2 py-1 border-b border-border/50 ${x?O:``}`,children:h?(0,k.jsxs)(`span`,{className:`flex items-center gap-1 whitespace-nowrap`,children:[(0,k.jsx)(`button`,{type:`button`,onClick:()=>g(t),className:`text-destructive text-[10px] font-medium hover:underline`,children:`Confirm`}),(0,k.jsx)(`button`,{type:`button`,onClick:()=>_(null),className:`text-muted-foreground text-[10px] hover:underline`,children:`Cancel`})]}):(0,k.jsx)(`button`,{type:`button`,onClick:()=>_(t),className:`p-0.5 rounded md:opacity-0 md:group-hover:opacity-100 hover:bg-destructive/10 hover:text-destructive transition-opacity`,title:`Delete row`,children:(0,k.jsx)(p,{className:`size-3`})})})]})});function F(e){let t={},n=/"(\w+)"\s+ILIKE\s+'%([^']*?)%'/gi,r;for(;(r=n.exec(e))!==null;)t[r[1]]=r[2].replace(/''/g,`'`);return t}function I({metadata:e}){let t=e?.connectionId,n=e?.connectionName,r=e?.tableName,i=e?.schemaName??`public`,a=e?.initialSql,o=ne(t),[s,l]=(0,D.useState)([]),[u,d]=(0,D.useState)(180),p=(0,D.useRef)(null),[h,g]=(0,D.useState)({}),_=(0,D.useMemo)(()=>{if(a&&!o.selectedTable)return a;if(o.selectedTable){let e=`SELECT * FROM "${o.selectedTable}"`,t=Object.entries(h).filter(([,e])=>e.trim()).map(([e,t])=>`"${e}" ILIKE '%${t.replace(/'/g,`''`)}%'`);t.length>0&&(e+=` WHERE ${t.join(` AND `)}`),o.orderBy&&(e+=` ORDER BY "${o.orderBy}" ${o.orderDir}`);let n=(o.page-1)*100;return e+=` LIMIT 100`,n>0&&(e+=` OFFSET ${n}`),e}return`SELECT * FROM `},[a,o.selectedTable,o.orderBy,o.orderDir,o.page,h]),v=(0,D.useCallback)(e=>{g(e)},[]),y=(0,D.useRef)(void 0);(0,D.useEffect)(()=>{if(!(!o.selectedTable||Object.keys(h).length===0))return clearTimeout(y.current),y.current=setTimeout(()=>{o.queryAsTable(_)},500),()=>clearTimeout(y.current)},[h]);let x=(0,D.useCallback)(e=>{e.preventDefault();let t=e.clientY,n=u,r=e=>{let r=e.clientY-t;d(Math.max(80,Math.min(n+r,(p.current?.clientHeight??600)-100)))},i=()=>{document.removeEventListener(`mousemove`,r),document.removeEventListener(`mouseup`,i)};document.addEventListener(`mousemove`,r),document.addEventListener(`mouseup`,i)},[u]);(0,D.useEffect)(()=>{m.get(`/api/db/connections/${t}/tables?cached=1`).then(e=>l(e.map(e=>({name:e.name,schema:e.schema})))).catch(()=>{})},[t]);let S=(0,D.useMemo)(()=>{if(s.length!==0)return{tables:s,getColumns:async(e,n)=>await m.get(`/api/db/connections/${t}/schema?table=${encodeURIComponent(e)}${n?`&schema=${encodeURIComponent(n)}`:``}`)}},[t,s]),w=(0,D.useRef)(!1);(0,D.useEffect)(()=>{w.current||(w.current=!0,a?o.executeQuery(a):r&&o.selectTable(r,i))},[r,i,a]);let[T,E]=(0,D.useState)(!!a),O=(0,D.useCallback)(e=>{let t=e.trim();if(o.selectedTable&&RegExp(`^SELECT\\s+\\*\\s+FROM\\s+"?${o.selectedTable.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}"?\\b`,`i`).test(t)){g(F(t)),o.queryAsTable(t);return}E(!0),o.executeQuery(e)},[o.executeQuery,o.queryAsTable,o.selectedTable]),ee=(0,D.useCallback)(e=>{E(!1),o.toggleSort(e)},[o.toggleSort]),te=(0,D.useCallback)(e=>{E(!1),o.setPage(e)},[o.setPage]),A=o.queryResult,M=T&&!!(A||o.queryError),N=o.selectedTable&&!M;return(0,k.jsx)(`div`,{ref:p,className:`flex h-full w-full overflow-hidden`,children:(0,k.jsxs)(`div`,{className:`flex-1 flex flex-col overflow-hidden`,children:[(0,k.jsxs)(`div`,{className:`flex items-center gap-2 px-3 py-1.5 border-b border-border bg-background shrink-0`,children:[(0,k.jsx)(c,{className:`size-3.5 text-muted-foreground`}),(0,k.jsx)(`span`,{className:`text-xs text-muted-foreground truncate`,children:n??`Database`}),o.selectedTable&&(0,k.jsxs)(`span`,{className:`text-xs text-muted-foreground`,children:[`/ `,o.selectedTable]}),(0,k.jsxs)(`div`,{className:`ml-auto flex items-center gap-1`,children:[o.tableData&&(0,k.jsx)(re,{columns:o.tableData.columns,rows:o.tableData.rows,filename:n?`${n}-${o.selectedTable??`data`}`:o.selectedTable??`data`,exportAllUrl:o.selectedTable?`/api/db/connections/${t}/export?table=${encodeURIComponent(o.selectedTable)}&schema=${o.selectedSchema}`:void 0}),(0,k.jsx)(`button`,{type:`button`,onClick:()=>o.refreshData(),title:`Reload data`,className:`p-1 rounded text-muted-foreground hover:text-foreground transition-colors`,children:(0,k.jsx)(f,{className:`size-3 ${o.loading?`animate-spin`:``}`})})]})]}),(0,k.jsx)(`div`,{className:`shrink-0 border-b border-border`,style:{height:u},children:(0,k.jsx)(C,{onExecute:O,loading:o.queryLoading,defaultValue:_,schemaInfo:S})}),(0,k.jsx)(`div`,{onMouseDown:x,className:`shrink-0 h-1.5 cursor-row-resize bg-border/50 hover:bg-primary/30 flex items-center justify-center transition-colors`,children:(0,k.jsx)(b,{className:`size-3 text-muted-foreground/50`})}),(0,k.jsxs)(`div`,{className:`flex-1 overflow-hidden`,children:[N&&(0,k.jsx)(j,{tableData:o.tableData,schema:o.schema,loading:o.loading,page:o.page,onPageChange:te,onCellUpdate:o.updateCell,onRowDelete:o.deleteRow,orderBy:o.orderBy,orderDir:o.orderDir,onToggleSort:ee,onBulkDelete:o.bulkDelete,onInsertRow:o.insertRow,connectionId:t,selectedTable:o.selectedTable,selectedSchema:o.selectedSchema,connectionName:n,columnFilters:h,onColumnFilter:v}),M&&(0,k.jsx)(R,{result:A,error:o.queryError,loading:o.queryLoading,schema:o.schema,connectionName:n})]})]})})}var L=()=>{};function R({result:e,error:t,loading:n,schema:r,connectionName:i}){let a=(0,D.useMemo)(()=>e?.changeType===`select`&&e.rows.length>0?{columns:e.columns,rows:e.rows,total:e.rows.length,limit:e.rows.length}:null,[e]),o=(0,D.useMemo)(()=>r?.length?r:(e?.columns??[]).map(e=>({name:e,type:`text`,nullable:!0,pk:!1,defaultValue:null})),[r,e?.columns]);return(0,k.jsxs)(`div`,{className:`flex flex-col h-full overflow-hidden text-xs`,children:[t&&(0,k.jsx)(`div`,{className:`px-3 py-2 text-destructive bg-destructive/5 shrink-0`,children:t}),e?.changeType===`modify`&&(0,k.jsxs)(`div`,{className:`px-3 py-2 text-green-500 shrink-0`,children:[e.rowsAffected,` row(s) affected`,e.executionTimeMs!=null&&(0,k.jsxs)(`span`,{className:`text-muted-foreground ml-2`,children:[e.executionTimeMs,`ms`]})]}),a&&(0,k.jsxs)(`div`,{className:`flex-1 overflow-hidden`,children:[(0,k.jsx)(j,{tableData:a,schema:o,loading:!!n,page:1,onPageChange:L,onCellUpdate:L,orderBy:null,orderDir:`ASC`,onToggleSort:L,connectionName:i}),e?.executionTimeMs!=null&&(0,k.jsxs)(`div`,{className:`px-3 py-0.5 border-t border-border text-[10px] text-muted-foreground shrink-0`,children:[e.rows.length,` rows · `,e.executionTimeMs,`ms`]})]}),e?.changeType===`select`&&e.rows.length===0&&(0,k.jsxs)(`div`,{className:`px-3 py-2 text-muted-foreground shrink-0`,children:[`No results`,e.executionTimeMs!=null&&(0,k.jsxs)(`span`,{className:`ml-2 text-muted-foreground/60`,children:[e.executionTimeMs,`ms`]})]}),!e&&!t&&(0,k.jsx)(`div`,{className:`flex items-center justify-center h-full text-muted-foreground`,children:n?(0,k.jsx)(v,{className:`size-4 animate-spin`}):`Run a query to see results`})]})}export{I as DatabaseViewer};
|