@hienlh/ppm 0.9.65 → 0.9.67
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 +26 -0
- package/dist/web/assets/{_basePickBy-3Xe18azI.js → _basePickBy-5PGDJbfF.js} +1 -1
- package/dist/web/assets/{_baseUniq-Yy35llnn.js → _baseUniq-BT4Ow4Kk.js} +1 -1
- package/dist/web/assets/{api-settings-CgBII8jW.js → api-settings-Bn-bIxD1.js} +1 -1
- package/dist/web/assets/{arc-B9n1Gvb5.js → arc-BAOivWpI.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-DqAZP_F6.js → architectureDiagram-2XIMDMQ5-Z-4eN4za.js} +1 -1
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-h3cDF2vI.js → blockDiagram-WCTKOSBZ-BCLqzhuZ.js} +1 -1
- package/dist/web/assets/{c4Diagram-IC4MRINW--pF1r5lr.js → c4Diagram-IC4MRINW-0Vp0Jeas.js} +1 -1
- package/dist/web/assets/channel-By7bn0Yq.js +1 -0
- package/dist/web/assets/chat-tab-onkz52iv.js +10 -0
- package/dist/web/assets/{chunk-4BX2VUAB-C3aZvW7B.js → chunk-4BX2VUAB-D4tOov49.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-D5cABeB9.js → chunk-55IACEB6-DJ6BynZ4.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-CkFGv6Zs.js → chunk-7E7YKBS2-CiyUJxNI.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-Dvbyu4Zw.js → chunk-7R4GIKGN-Dv-4cAYn.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-CtqKiH4q.js → chunk-C72U2L5F-D21mS_6G.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-Cpr87sBR.js → chunk-EGIJ26TM-DzqmU2Z7.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-D23YVTOU.js → chunk-FMBD7UC4-DXncblvW.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-tDjHsAUs.js → chunk-GEFDOKGD-D-pKjlVd.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-DKikpoJM.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-C7vxA5i9.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-BBmymCjA.js → chunk-JSJVCQXG-99JzIdPr.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-DP36BDiU.js → chunk-KX2RTZJC-CRq1OBZv.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-Djw13C-3.js → chunk-KYZI473N-Bb0MCaIO.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL-HG_eMj_C.js → chunk-L3YUKLVL-C7qGJrfV.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-C2UEioMs.js → chunk-MX3YWQON-BpS_PtKp.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-DXUTQ-BL.js → chunk-NQ4KR5QH-z_blpjxi.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-BsUWb9d0.js → chunk-O4XLMI2P-nDhi_cVu.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-rG0P22U9.js → chunk-OZEHJAEY-BXhYx3nO.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-DX0xW7kO.js → chunk-PQ6SQG4A-TF58UVMU.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-C7Gry6md.js → chunk-PU5JKC2W-ek7k4QVB.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-CMY0PkRK.js → chunk-R5LLSJPH-CFwSJijQ.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-CXuQvlyu.js → chunk-WL4C6EOR-ByUrSRin.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-DRJEb7Zb.js → chunk-XIRO2GV7-Djlmrely.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-BPEX8KhL.js → chunk-XPW4576I-BPQQBakK.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-Cb0iqycX.js → chunk-XZSTWKYB-DxAOx4hG.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-av5aeHLq.js → chunk-YBOYWFTD-rQG3QH5s.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-BA8Nj-_C.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-DjYu-6mn.js +1 -0
- package/dist/web/assets/clone-LRxlvnMj.js +1 -0
- package/dist/web/assets/code-editor-BixOXePn.js +8 -0
- package/dist/web/assets/{cose-bilkent-S5V4N54A-qudEiMCT.js → cose-bilkent-S5V4N54A-B_AWZsOP.js} +1 -1
- package/dist/web/assets/csv-parser-CNNw2RVA.js +6 -0
- package/dist/web/assets/{csv-preview-sx6DC51G.js → csv-preview-D2pJJj3K.js} +3 -8
- package/dist/web/assets/{dagre-BFcnKyBF.js → dagre-DHq9bhnd.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-C3O-MTLf.js → dagre-KLK3FWXG-BdJr7Byp.js} +1 -1
- package/dist/web/assets/database-viewer-DfLe8ewt.js +2 -0
- package/dist/web/assets/{diagram-E7M64L7V-DxPjK7_c.js → diagram-E7M64L7V-_db4pBVA.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-sqTog_XV.js → diagram-IFDJBPK2-xKoeuiJx.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-hzmp0GHK.js → diagram-P4PSJMXO-C8tjJsev.js} +1 -1
- package/dist/web/assets/diff-viewer-eFO08m_L.js +4 -0
- package/dist/web/assets/dist-DIV6WgAG.js +41 -0
- package/dist/web/assets/{erDiagram-INFDFZHY-DLeYhAAT.js → erDiagram-INFDFZHY-BSh2z9Df.js} +1 -1
- package/dist/web/assets/{extension-webview-2XjXXtyy.js → extension-webview-4CL9kCKR.js} +1 -1
- package/dist/web/assets/{flowDiagram-PKNHOUZH-CRxlE9Sr.js → flowDiagram-PKNHOUZH-oYaovqyp.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-BdjmoMLS.js → ganttDiagram-A5KZAMGK-DmL26q2P.js} +1 -1
- package/dist/web/assets/git-graph-BqeE_o17.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-BeHSX7kk.js → gitGraphDiagram-K3NZZRJ6-CMoukSrY.js} +1 -1
- package/dist/web/assets/{graphlib-Duh_bWLa.js → graphlib-BcsNnGcW.js} +1 -1
- package/dist/web/assets/index-BWLy2h18.css +2 -0
- package/dist/web/assets/index-DwrCg0TN.js +30 -0
- package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-DWwumDkq.js +2 -0
- package/dist/web/assets/{isEmpty-B9L-Ge-H.js → isEmpty-bnrF3Qbc.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-Cu0Rt1Ok.js → ishikawaDiagram-PHBUUO56-D05_LyL7.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-CgDI-UG4.js → journeyDiagram-4ABVD52K-B_L20qMe.js} +1 -1
- package/dist/web/assets/{kanban-definition-K7BYSVSG-h4g10UHL.js → kanban-definition-K7BYSVSG-CZ535BbZ.js} +1 -1
- package/dist/web/assets/keybindings-store-BNBONtSd.js +1 -0
- package/dist/web/assets/{line-B75-Rx70.js → line-CVvo3dRu.js} +1 -1
- package/dist/web/assets/{linear-Bcjv9FQt.js → linear-DP4mkX3m.js} +1 -1
- package/dist/web/assets/{markdown-renderer-Bb7OSpxF.js → markdown-renderer-BUqab2os.js} +5 -5
- package/dist/web/assets/{mermaid-parser.core-8u2leTXI.js → mermaid-parser.core-C7UwoIh6.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-BaOBwb-W.js → mindmap-definition-YRQLILUH-x0MTutJp.js} +1 -1
- package/dist/web/assets/{ordinal-LFEjVtwQ.js → ordinal-_K3x1fkz.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-At5Kz0KK.js → pieDiagram-SKSYHLDU-C1Gjrtzy.js} +1 -1
- package/dist/web/assets/{port-forwarding-tab-bD8MKumH.js → port-forwarding-tab-CfO-UJ84.js} +1 -1
- package/dist/web/assets/postgres-viewer-BVJZ44eU.js +13 -0
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-CdjGIDfw.js → quadrantDiagram-337W2JSQ-C8bzJCjQ.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-B9F_Cx_p.js → requirementDiagram-Z7DCOOCP-pQyah6WB.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-RolPi8bU.js → sankeyDiagram-WA2Y5GQK-T6RgG-N8.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-DM-tMAhx.js → sequenceDiagram-2WXFIKYE-BQDJ4CVs.js} +1 -1
- package/dist/web/assets/settings-tab-C6hdJujW.js +1 -0
- package/dist/web/assets/sql-completion-provider-DM9Qov6L.js +1 -0
- package/dist/web/assets/sql-query-editor-OhZa4Z9F.js +3 -0
- package/dist/web/assets/sqlite-viewer-C8p1_jz4.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-C4EMl6jf.js → stateDiagram-RAJIS63D-66vhiIuk.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-BGVqj_g9.js +1 -0
- package/dist/web/assets/{terminal-tab-Cq6vQ9W9.js → terminal-tab-CaO0WnIo.js} +2 -2
- package/dist/web/assets/text-wrap-BWNOVswA.js +1 -0
- package/dist/web/assets/{timeline-definition-YZTLITO2-A4PN_Efm.js → timeline-definition-YZTLITO2-DwZqB3nn.js} +1 -1
- package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +1 -0
- package/dist/web/assets/use-monaco-theme-U9ZhfvHB.js +11 -0
- package/dist/web/assets/{vennDiagram-LZ73GAT5-ywK7LMaH.js → vennDiagram-LZ73GAT5-s9Z71fz-.js} +1 -1
- package/dist/web/assets/x-D2_KzIET.js +1 -0
- package/dist/web/assets/{xychartDiagram-JWTSCODW-DylHYNtJ.js → xychartDiagram-JWTSCODW-DRa_TH4B.js} +1 -1
- package/dist/web/index.html +9 -8
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/cli/commands/bot-cmd.ts +4 -0
- package/src/cli/commands/db-cmd.ts +4 -3
- package/src/server/routes/database.ts +126 -9
- package/src/services/cloud.service.ts +2 -1
- package/src/services/database/sqlite-adapter.ts +1 -0
- package/src/services/db.service.ts +42 -3
- package/src/services/postgres.service.ts +37 -6
- package/src/services/sqlite.service.ts +18 -4
- package/src/services/table-cache.service.ts +2 -3
- package/src/types/database.ts +2 -0
- package/src/web/components/database/connection-form-dialog.tsx +17 -8
- package/src/web/components/database/connection-list.tsx +191 -139
- package/src/web/components/database/data-grid.tsx +634 -0
- package/src/web/components/database/database-sidebar.tsx +4 -1
- package/src/web/components/database/database-viewer.tsx +204 -225
- package/src/web/components/database/export-button.tsx +100 -0
- package/src/web/components/database/sql-completion-provider.ts +301 -0
- package/src/web/components/database/sql-query-editor.tsx +123 -0
- package/src/web/components/database/use-connections.ts +21 -1
- package/src/web/components/database/use-database.ts +59 -7
- package/src/web/components/editor/code-editor.tsx +224 -16
- package/src/web/components/sqlite/sqlite-query-editor.tsx +3 -90
- package/src/web/components/sqlite/sqlite-viewer.tsx +0 -2
- package/src/web/components/sqlite/use-sqlite.ts +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-CFzkFKEL.js +0 -1
- package/dist/web/assets/channel-C2fMafck.js +0 -1
- package/dist/web/assets/chat-tab-CfdMDCBK.js +0 -10
- package/dist/web/assets/chunk-GLR3WWYH-DBdWQ3zy.js +0 -2
- package/dist/web/assets/chunk-HHEYEP7N-BBw_z0fW.js +0 -1
- package/dist/web/assets/chunk-QZHKN3VN-DFKFM_C1.js +0 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-Dp4Kk3Yb.js +0 -1
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-D8IvcV_B.js +0 -1
- package/dist/web/assets/clone-B2hUek6n.js +0 -1
- package/dist/web/assets/code-editor-DhCfJIpG.js +0 -2
- package/dist/web/assets/database-viewer-DLkAUBpm.js +0 -1
- package/dist/web/assets/diff-viewer-DWVWsekJ.js +0 -4
- package/dist/web/assets/dist-C40JmyoH.js +0 -13
- package/dist/web/assets/dist-DRTW9IWi.js +0 -41
- package/dist/web/assets/git-graph-Ds5bs1cM.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-CNlas3Rz.js +0 -1
- package/dist/web/assets/index-8b0LM6IC.js +0 -30
- package/dist/web/assets/index-BnMECpW3.css +0 -2
- package/dist/web/assets/info-3K5VOQVL-BDzTLc11.js +0 -1
- package/dist/web/assets/infoDiagram-LFFYTUFH-ZZmpgc6t.js +0 -2
- package/dist/web/assets/keybindings-store-C06Z0Zhk.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-IVa5F-go.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-CvXHKAzp.js +0 -1
- package/dist/web/assets/postgres-viewer-C3OND65T.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-Z-Tr5wtS.js +0 -1
- package/dist/web/assets/settings-tab-cuMIkUNV.js +0 -1
- package/dist/web/assets/sqlite-viewer-CpjnwDtk.js +0 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-B-UjZch3.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-C9TYRE0k.js +0 -1
- package/dist/web/assets/use-monaco-theme-BH9sQ-Yu.js +0 -11
- /package/dist/web/assets/{api-client-BKIT_Qeg.js → api-client-BfBM3I7n.js} +0 -0
- /package/dist/web/assets/{array-DqLCdDFv.js → array-B9UHiPd-.js} +0 -0
- /package/dist/web/assets/{chevron-right-5HgK6l7K.js → chevron-right-4zq1jPv6.js} +0 -0
- /package/dist/web/assets/{columns-2-cEVJHYd7.js → columns-2-BoZAN-iw.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-CWPXKqbJ.js → cytoscape.esm-BW-DbntU.js} +0 -0
- /package/dist/web/assets/{defaultLocale-CrJzLgRD.js → defaultLocale-5eAKkKJC.js} +0 -0
- /package/dist/web/assets/{dist-Cep75xXf.js → dist-CSJdAyA9.js} +0 -0
- /package/dist/web/assets/{init-C0r9Gk5G.js → init-DlZdxViB.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-CGBoxvCD.js → isArrayLikeObject-B_v2FtYn.js} +0 -0
- /package/dist/web/assets/{katex-DzXRfQ_m.js → katex-Bqvo_ZG0.js} +0 -0
- /package/dist/web/assets/{lib-mag4ySk-.js → lib-DurwGtQO.js} +0 -0
- /package/dist/web/assets/{math-y9zN1W-N.js → math-069Z4SuC.js} +0 -0
- /package/dist/web/assets/{path-DIKpVbHL.js → path-6uRLdFF7.js} +0 -0
- /package/dist/web/assets/{rough.esm-nHaDi0Kw.js → rough.esm-JX0wREDd.js} +0 -0
- /package/dist/web/assets/{src-Dw4QhedI.js → src-BqX54PbV.js} +0 -0
- /package/dist/web/assets/{utils-DMiycH3O.js → utils-BNytJOb1.js} +0 -0
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { useState, useMemo } from "react";
|
|
2
|
-
import { ChevronRight, ChevronDown, Database, RefreshCw, Pencil, Trash2, Lock, Search } from "lucide-react";
|
|
2
|
+
import { ChevronRight, ChevronDown, Database, RefreshCw, Pencil, Trash2, Lock, Search, Key, Link2 } from "lucide-react";
|
|
3
3
|
import { cn } from "@/lib/utils";
|
|
4
4
|
import type { Connection, CachedTable } from "./use-connections";
|
|
5
5
|
|
|
6
|
+
interface ColumnInfo {
|
|
7
|
+
name: string;
|
|
8
|
+
type: string;
|
|
9
|
+
nullable: boolean;
|
|
10
|
+
pk: boolean;
|
|
11
|
+
fk: { table: string; column: string } | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
interface ConnectionListProps {
|
|
7
15
|
connections: Connection[];
|
|
8
16
|
cachedTables: Map<number, CachedTable[]>;
|
|
@@ -10,6 +18,8 @@ interface ConnectionListProps {
|
|
|
10
18
|
onRefreshTables: (id: number) => Promise<void>;
|
|
11
19
|
onEdit: (conn: Connection) => void;
|
|
12
20
|
onDelete: (id: number) => void;
|
|
21
|
+
onFetchColumns?: (connId: number, table: string, schema?: string) => Promise<ColumnInfo[]>;
|
|
22
|
+
columnCache?: Map<string, ColumnInfo[]>;
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
interface GroupMap {
|
|
@@ -19,11 +29,14 @@ interface GroupMap {
|
|
|
19
29
|
export function ConnectionList({
|
|
20
30
|
connections, cachedTables,
|
|
21
31
|
onOpenTable, onRefreshTables, onEdit, onDelete,
|
|
32
|
+
onFetchColumns, columnCache,
|
|
22
33
|
}: ConnectionListProps) {
|
|
23
34
|
const [expandedConns, setExpandedConns] = useState<Set<number>>(new Set());
|
|
24
35
|
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set(["__ungrouped__"]));
|
|
36
|
+
const [expandedTables, setExpandedTables] = useState<Set<string>>(new Set());
|
|
25
37
|
const [refreshingIds, setRefreshingIds] = useState<Set<number>>(new Set());
|
|
26
38
|
const [tableFilter, setTableFilter] = useState<Map<number, string>>(new Map());
|
|
39
|
+
const [loadingColumns, setLoadingColumns] = useState<Set<string>>(new Set());
|
|
27
40
|
|
|
28
41
|
const toggleConn = (id: number) => {
|
|
29
42
|
setExpandedConns((prev) => {
|
|
@@ -41,6 +54,21 @@ export function ConnectionList({
|
|
|
41
54
|
});
|
|
42
55
|
};
|
|
43
56
|
|
|
57
|
+
const toggleTable = async (connId: number, tableName: string, schemaName: string) => {
|
|
58
|
+
const key = `${connId}:${schemaName}.${tableName}`;
|
|
59
|
+
if (expandedTables.has(key)) {
|
|
60
|
+
setExpandedTables((prev) => { const n = new Set(prev); n.delete(key); return n; });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
setExpandedTables((prev) => new Set(prev).add(key));
|
|
64
|
+
// Lazy load columns if not cached
|
|
65
|
+
if (onFetchColumns && !columnCache?.has(key)) {
|
|
66
|
+
setLoadingColumns((prev) => new Set(prev).add(key));
|
|
67
|
+
try { await onFetchColumns(connId, tableName, schemaName); } catch { /* ignore */ }
|
|
68
|
+
setLoadingColumns((prev) => { const n = new Set(prev); n.delete(key); return n; });
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
44
72
|
const handleRefresh = async (id: number) => {
|
|
45
73
|
setRefreshingIds((p) => new Set(p).add(id));
|
|
46
74
|
try { await onRefreshTables(id); } finally {
|
|
@@ -74,12 +102,10 @@ export function ConnectionList({
|
|
|
74
102
|
const isGroupExpanded = expandedGroups.has(group);
|
|
75
103
|
const label = group === "__ungrouped__" ? "Ungrouped" : group;
|
|
76
104
|
const groupConns = groups[group]!;
|
|
77
|
-
|
|
78
105
|
const hasGroup = groupKeys.length > 1 || group !== "__ungrouped__";
|
|
79
106
|
|
|
80
107
|
return (
|
|
81
108
|
<div key={group}>
|
|
82
|
-
{/* Group header (only shown when there are multiple groups or named group) */}
|
|
83
109
|
{hasGroup && (
|
|
84
110
|
<button
|
|
85
111
|
onClick={() => toggleGroup(group)}
|
|
@@ -90,112 +116,104 @@ export function ConnectionList({
|
|
|
90
116
|
</button>
|
|
91
117
|
)}
|
|
92
118
|
|
|
93
|
-
{/* Connections — indented with tree guide line when inside a group */}
|
|
94
119
|
{isGroupExpanded && (
|
|
95
120
|
<div className={hasGroup ? "ml-[11px] border-l border-dashed border-border" : ""}>
|
|
96
121
|
{groupConns.map((conn) => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<div key={conn.id}>
|
|
103
|
-
{/* Connection row */}
|
|
104
|
-
<div className={cn("group flex items-center gap-1 py-1 hover:bg-surface-elevated transition-colors", hasGroup ? "pl-3 pr-2" : "px-2")}>
|
|
105
|
-
{/* Expand arrow */}
|
|
106
|
-
<button
|
|
107
|
-
onClick={() => {
|
|
108
|
-
toggleConn(conn.id);
|
|
109
|
-
if (!expandedConns.has(conn.id) && tables.length === 0) {
|
|
110
|
-
handleRefresh(conn.id);
|
|
111
|
-
}
|
|
112
|
-
}}
|
|
113
|
-
className="shrink-0 text-text-subtle hover:text-foreground transition-colors"
|
|
114
|
-
>
|
|
115
|
-
{isExpanded ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
|
|
116
|
-
</button>
|
|
117
|
-
|
|
118
|
-
{/* Color dot */}
|
|
119
|
-
<span
|
|
120
|
-
className="shrink-0 size-2 rounded-full border border-border"
|
|
121
|
-
style={{ backgroundColor: conn.color ?? "transparent" }}
|
|
122
|
-
/>
|
|
123
|
-
|
|
124
|
-
{/* Name — click toggles expand */}
|
|
125
|
-
<button
|
|
126
|
-
className="flex-1 text-left text-xs truncate hover:text-primary transition-colors"
|
|
127
|
-
onClick={() => {
|
|
128
|
-
toggleConn(conn.id);
|
|
129
|
-
if (!expandedConns.has(conn.id) && tables.length === 0) {
|
|
130
|
-
handleRefresh(conn.id);
|
|
131
|
-
}
|
|
132
|
-
}}
|
|
133
|
-
>
|
|
134
|
-
{conn.name}
|
|
135
|
-
</button>
|
|
122
|
+
const isExpanded = expandedConns.has(conn.id);
|
|
123
|
+
const tables = cachedTables.get(conn.id) ?? [];
|
|
124
|
+
const isRefreshing = refreshingIds.has(conn.id);
|
|
136
125
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
126
|
+
// Group tables by schema for postgres
|
|
127
|
+
const schemas = useMemo(() => {
|
|
128
|
+
const map = new Map<string, CachedTable[]>();
|
|
129
|
+
for (const t of tables) {
|
|
130
|
+
const key = t.schemaName;
|
|
131
|
+
(map.get(key) ?? map.set(key, []).get(key)!).push(t);
|
|
132
|
+
}
|
|
133
|
+
return map;
|
|
134
|
+
}, [tables]);
|
|
141
135
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
<span title="Readonly">
|
|
145
|
-
<Lock className="shrink-0 size-2.5 text-text-subtle" aria-label="Readonly" />
|
|
146
|
-
</span>
|
|
147
|
-
)}
|
|
136
|
+
const isSingleSchema = schemas.size <= 1;
|
|
137
|
+
const filter = tableFilter.get(conn.id) ?? "";
|
|
148
138
|
|
|
149
|
-
|
|
150
|
-
<div
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
139
|
+
return (
|
|
140
|
+
<div key={conn.id}>
|
|
141
|
+
{/* Connection row */}
|
|
142
|
+
<div className={cn("group flex items-center gap-1 py-1 hover:bg-surface-elevated transition-colors", hasGroup ? "pl-3 pr-2" : "px-2")}>
|
|
143
|
+
<button
|
|
144
|
+
onClick={() => {
|
|
145
|
+
toggleConn(conn.id);
|
|
146
|
+
if (!expandedConns.has(conn.id) && tables.length === 0) handleRefresh(conn.id);
|
|
147
|
+
}}
|
|
148
|
+
className="shrink-0 text-text-subtle hover:text-foreground transition-colors"
|
|
149
|
+
>
|
|
150
|
+
{isExpanded ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
|
|
151
|
+
</button>
|
|
152
|
+
<span className="shrink-0 size-2 rounded-full border border-border" style={{ backgroundColor: conn.color ?? "transparent" }} />
|
|
153
|
+
<button
|
|
154
|
+
className="flex-1 text-left text-xs truncate hover:text-primary transition-colors"
|
|
155
|
+
onClick={() => {
|
|
156
|
+
toggleConn(conn.id);
|
|
157
|
+
if (!expandedConns.has(conn.id) && tables.length === 0) handleRefresh(conn.id);
|
|
158
|
+
}}
|
|
159
|
+
>
|
|
160
|
+
{conn.name}
|
|
161
|
+
</button>
|
|
162
|
+
<span className="shrink-0 text-[9px] text-text-subtle uppercase px-1 rounded bg-surface-elevated">
|
|
163
|
+
{conn.type === "postgres" ? "PG" : "DB"}
|
|
164
|
+
</span>
|
|
165
|
+
{conn.readonly === 1 && <span title="Readonly"><Lock className="shrink-0 size-2.5 text-text-subtle" /></span>}
|
|
166
|
+
<div className="flex can-hover:hidden can-hover:group-hover:flex items-center gap-0.5 shrink-0">
|
|
167
|
+
<button onClick={() => handleRefresh(conn.id)} disabled={isRefreshing} className="p-0.5 text-text-subtle hover:text-foreground transition-colors" title="Refresh tables">
|
|
168
|
+
<RefreshCw className={cn("size-3", isRefreshing && "animate-spin")} />
|
|
169
|
+
</button>
|
|
170
|
+
<button onClick={() => onEdit(conn)} className="p-0.5 text-text-subtle hover:text-foreground transition-colors" title="Edit">
|
|
171
|
+
<Pencil className="size-3" />
|
|
172
|
+
</button>
|
|
173
|
+
<button onClick={() => onDelete(conn.id)} className="p-0.5 text-text-subtle hover:text-red-500 transition-colors" title="Delete">
|
|
174
|
+
<Trash2 className="size-3" />
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
175
178
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
179
|
+
{/* Expanded tree: schemas > tables > columns */}
|
|
180
|
+
{isExpanded && (
|
|
181
|
+
<div className="ml-[11px] border-l border-dashed border-border pl-1">
|
|
182
|
+
{isRefreshing && tables.length === 0 && <p className="text-[10px] text-text-subtle px-2 py-1">Loading…</p>}
|
|
183
|
+
{!isRefreshing && tables.length === 0 && <p className="text-[10px] text-text-subtle px-2 py-1">No tables cached</p>}
|
|
184
|
+
{tables.length > 0 && (
|
|
185
|
+
<>
|
|
186
|
+
{tables.length > 5 && (
|
|
187
|
+
<div className="flex items-center gap-1 px-2 py-0.5">
|
|
188
|
+
<Search className="size-2.5 text-text-subtle shrink-0" />
|
|
189
|
+
<input
|
|
190
|
+
type="text"
|
|
191
|
+
value={filter}
|
|
192
|
+
onChange={(e) => setTableFilter((prev) => new Map(prev).set(conn.id, e.target.value))}
|
|
193
|
+
placeholder="Filter tables…"
|
|
194
|
+
className="w-full text-[10px] bg-transparent border-none outline-none text-foreground placeholder:text-text-subtle"
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
<SchemaTableTree
|
|
199
|
+
connId={conn.id}
|
|
200
|
+
connType={conn.type}
|
|
201
|
+
schemas={schemas}
|
|
202
|
+
isSingleSchema={isSingleSchema}
|
|
203
|
+
filter={filter}
|
|
204
|
+
expandedTables={expandedTables}
|
|
205
|
+
loadingColumns={loadingColumns}
|
|
206
|
+
columnCache={columnCache}
|
|
207
|
+
onToggleTable={toggleTable}
|
|
208
|
+
onOpenTable={(tableName, schemaName) => onOpenTable(conn, tableName, schemaName)}
|
|
209
|
+
/>
|
|
210
|
+
</>
|
|
211
|
+
)}
|
|
212
|
+
</div>
|
|
193
213
|
)}
|
|
194
214
|
</div>
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
);
|
|
198
|
-
})}
|
|
215
|
+
);
|
|
216
|
+
})}
|
|
199
217
|
</div>
|
|
200
218
|
)}
|
|
201
219
|
</div>
|
|
@@ -205,53 +223,87 @@ export function ConnectionList({
|
|
|
205
223
|
);
|
|
206
224
|
}
|
|
207
225
|
|
|
208
|
-
/* ---------- Table
|
|
209
|
-
const MAX_TABLE_HEIGHT = 200; // px
|
|
226
|
+
/* ---------- Schema > Table > Column tree ---------- */
|
|
210
227
|
|
|
211
|
-
function
|
|
228
|
+
function SchemaTableTree({ connId, connType, schemas, isSingleSchema, filter, expandedTables, loadingColumns, columnCache, onToggleTable, onOpenTable }: {
|
|
212
229
|
connId: number;
|
|
213
|
-
|
|
230
|
+
connType: "sqlite" | "postgres";
|
|
231
|
+
schemas: Map<string, CachedTable[]>;
|
|
232
|
+
isSingleSchema: boolean;
|
|
214
233
|
filter: string;
|
|
215
|
-
|
|
234
|
+
expandedTables: Set<string>;
|
|
235
|
+
loadingColumns: Set<string>;
|
|
236
|
+
columnCache?: Map<string, ColumnInfo[]>;
|
|
237
|
+
onToggleTable: (connId: number, tableName: string, schemaName: string) => void;
|
|
216
238
|
onOpenTable: (tableName: string, schemaName: string) => void;
|
|
217
239
|
}) {
|
|
218
|
-
const
|
|
219
|
-
if (!filter) return tables;
|
|
220
|
-
const q = filter.toLowerCase();
|
|
221
|
-
return tables.filter((t) => t.tableName.toLowerCase().includes(q));
|
|
222
|
-
}, [tables, filter]);
|
|
240
|
+
const filterLower = filter.toLowerCase();
|
|
223
241
|
|
|
224
242
|
return (
|
|
225
|
-
<div>
|
|
226
|
-
{
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
243
|
+
<div className="overflow-y-auto" style={{ maxHeight: 300 }}>
|
|
244
|
+
{Array.from(schemas.entries()).map(([schemaName, tables]) => {
|
|
245
|
+
const filteredTables = filterLower
|
|
246
|
+
? tables.filter((t) => t.tableName.toLowerCase().includes(filterLower))
|
|
247
|
+
: tables;
|
|
248
|
+
if (filteredTables.length === 0) return null;
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<div key={schemaName}>
|
|
252
|
+
{/* Schema label (only for postgres with multiple schemas) */}
|
|
253
|
+
{!isSingleSchema && (
|
|
254
|
+
<p className="px-2 py-0.5 text-[9px] font-semibold text-text-subtle uppercase tracking-wider">{schemaName}</p>
|
|
255
|
+
)}
|
|
256
|
+
{filteredTables.map((t) => {
|
|
257
|
+
const tableKey = `${connId}:${t.schemaName}.${t.tableName}`;
|
|
258
|
+
const isTableExpanded = expandedTables.has(tableKey);
|
|
259
|
+
const isLoadingCols = loadingColumns.has(tableKey);
|
|
260
|
+
const columns = columnCache?.get(tableKey);
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<div key={tableKey}>
|
|
264
|
+
{/* Table row */}
|
|
265
|
+
<div className="flex items-center gap-1 pl-2 pr-2 py-0.5 hover:bg-surface-elevated transition-colors group/table">
|
|
266
|
+
<button
|
|
267
|
+
onClick={() => onToggleTable(connId, t.tableName, t.schemaName)}
|
|
268
|
+
className="shrink-0 text-text-subtle hover:text-foreground transition-colors"
|
|
269
|
+
>
|
|
270
|
+
{isTableExpanded ? <ChevronDown className="size-2.5" /> : <ChevronRight className="size-2.5" />}
|
|
271
|
+
</button>
|
|
272
|
+
<Database className="size-2.5 shrink-0 text-text-subtle" />
|
|
273
|
+
<button
|
|
274
|
+
onClick={() => onOpenTable(t.tableName, t.schemaName)}
|
|
275
|
+
className="flex-1 text-left text-[11px] text-text-secondary hover:text-foreground transition-colors truncate"
|
|
276
|
+
>
|
|
277
|
+
{t.tableName}
|
|
278
|
+
</button>
|
|
279
|
+
<span className="text-[9px] text-text-subtle">{t.rowCount}</span>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
{/* Columns (lazy loaded) */}
|
|
283
|
+
{isTableExpanded && (
|
|
284
|
+
<div className="ml-[18px] border-l border-dotted border-border pl-2">
|
|
285
|
+
{isLoadingCols && <p className="text-[9px] text-text-subtle px-1 py-0.5">Loading…</p>}
|
|
286
|
+
{columns && columns.map((col) => (
|
|
287
|
+
<div key={col.name} className="flex items-center gap-1 px-1 py-px text-[10px] text-text-subtle" title={col.fk ? `FK → ${col.fk.table}.${col.fk.column}` : undefined}>
|
|
288
|
+
{col.pk && <Key className="size-2.5 text-amber-500 shrink-0" />}
|
|
289
|
+
{col.fk && <Link2 className="size-2.5 text-blue-400 shrink-0" />}
|
|
290
|
+
{!col.pk && !col.fk && <span className="size-2.5 shrink-0" />}
|
|
291
|
+
<span className="truncate">{col.name}{col.nullable ? "?" : ""}</span>
|
|
292
|
+
<span className="ml-auto text-[9px] text-text-subtle/60 shrink-0">{col.type}</span>
|
|
293
|
+
</div>
|
|
294
|
+
))}
|
|
295
|
+
{!isLoadingCols && !columns && <p className="text-[9px] text-text-subtle px-1 py-0.5">No columns</p>}
|
|
296
|
+
</div>
|
|
297
|
+
)}
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
})}
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
})}
|
|
304
|
+
{filter && Array.from(schemas.values()).every((t) => !t.some((x) => x.tableName.toLowerCase().includes(filterLower))) && (
|
|
305
|
+
<p className="text-[10px] text-text-subtle px-2 py-1">No match</p>
|
|
238
306
|
)}
|
|
239
|
-
{/* Scrollable table list */}
|
|
240
|
-
<div className="overflow-y-auto" style={{ maxHeight: MAX_TABLE_HEIGHT }}>
|
|
241
|
-
{filtered.map((t) => (
|
|
242
|
-
<button
|
|
243
|
-
key={`${connId}-${t.schemaName}.${t.tableName}`}
|
|
244
|
-
onClick={() => onOpenTable(t.tableName, t.schemaName)}
|
|
245
|
-
className="w-full flex items-center gap-1.5 px-2 py-0.5 text-[11px] text-text-secondary hover:text-foreground hover:bg-surface-elevated transition-colors text-left truncate"
|
|
246
|
-
>
|
|
247
|
-
<Database className="size-2.5 shrink-0 text-text-subtle" />
|
|
248
|
-
<span className="truncate">{t.tableName}</span>
|
|
249
|
-
</button>
|
|
250
|
-
))}
|
|
251
|
-
{filter && filtered.length === 0 && (
|
|
252
|
-
<p className="text-[10px] text-text-subtle px-2 py-1">No match</p>
|
|
253
|
-
)}
|
|
254
|
-
</div>
|
|
255
307
|
</div>
|
|
256
308
|
);
|
|
257
309
|
}
|