@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.
Files changed (173) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/web/assets/{_basePickBy-3Xe18azI.js → _basePickBy-5PGDJbfF.js} +1 -1
  3. package/dist/web/assets/{_baseUniq-Yy35llnn.js → _baseUniq-BT4Ow4Kk.js} +1 -1
  4. package/dist/web/assets/{api-settings-CgBII8jW.js → api-settings-Bn-bIxD1.js} +1 -1
  5. package/dist/web/assets/{arc-B9n1Gvb5.js → arc-BAOivWpI.js} +1 -1
  6. package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +1 -0
  7. package/dist/web/assets/{architectureDiagram-2XIMDMQ5-DqAZP_F6.js → architectureDiagram-2XIMDMQ5-Z-4eN4za.js} +1 -1
  8. package/dist/web/assets/{blockDiagram-WCTKOSBZ-h3cDF2vI.js → blockDiagram-WCTKOSBZ-BCLqzhuZ.js} +1 -1
  9. package/dist/web/assets/{c4Diagram-IC4MRINW--pF1r5lr.js → c4Diagram-IC4MRINW-0Vp0Jeas.js} +1 -1
  10. package/dist/web/assets/channel-By7bn0Yq.js +1 -0
  11. package/dist/web/assets/chat-tab-onkz52iv.js +10 -0
  12. package/dist/web/assets/{chunk-4BX2VUAB-C3aZvW7B.js → chunk-4BX2VUAB-D4tOov49.js} +1 -1
  13. package/dist/web/assets/{chunk-55IACEB6-D5cABeB9.js → chunk-55IACEB6-DJ6BynZ4.js} +1 -1
  14. package/dist/web/assets/{chunk-7E7YKBS2-CkFGv6Zs.js → chunk-7E7YKBS2-CiyUJxNI.js} +1 -1
  15. package/dist/web/assets/{chunk-7R4GIKGN-Dvbyu4Zw.js → chunk-7R4GIKGN-Dv-4cAYn.js} +2 -2
  16. package/dist/web/assets/{chunk-C72U2L5F-CtqKiH4q.js → chunk-C72U2L5F-D21mS_6G.js} +1 -1
  17. package/dist/web/assets/{chunk-EGIJ26TM-Cpr87sBR.js → chunk-EGIJ26TM-DzqmU2Z7.js} +1 -1
  18. package/dist/web/assets/{chunk-FMBD7UC4-D23YVTOU.js → chunk-FMBD7UC4-DXncblvW.js} +1 -1
  19. package/dist/web/assets/{chunk-GEFDOKGD-tDjHsAUs.js → chunk-GEFDOKGD-D-pKjlVd.js} +1 -1
  20. package/dist/web/assets/chunk-GLR3WWYH-DKikpoJM.js +2 -0
  21. package/dist/web/assets/chunk-HHEYEP7N-C7vxA5i9.js +1 -0
  22. package/dist/web/assets/{chunk-JSJVCQXG-BBmymCjA.js → chunk-JSJVCQXG-99JzIdPr.js} +1 -1
  23. package/dist/web/assets/{chunk-KX2RTZJC-DP36BDiU.js → chunk-KX2RTZJC-CRq1OBZv.js} +1 -1
  24. package/dist/web/assets/{chunk-KYZI473N-Djw13C-3.js → chunk-KYZI473N-Bb0MCaIO.js} +1 -1
  25. package/dist/web/assets/{chunk-L3YUKLVL-HG_eMj_C.js → chunk-L3YUKLVL-C7qGJrfV.js} +1 -1
  26. package/dist/web/assets/{chunk-MX3YWQON-C2UEioMs.js → chunk-MX3YWQON-BpS_PtKp.js} +1 -1
  27. package/dist/web/assets/{chunk-NQ4KR5QH-DXUTQ-BL.js → chunk-NQ4KR5QH-z_blpjxi.js} +1 -1
  28. package/dist/web/assets/{chunk-O4XLMI2P-BsUWb9d0.js → chunk-O4XLMI2P-nDhi_cVu.js} +1 -1
  29. package/dist/web/assets/{chunk-OZEHJAEY-rG0P22U9.js → chunk-OZEHJAEY-BXhYx3nO.js} +1 -1
  30. package/dist/web/assets/{chunk-PQ6SQG4A-DX0xW7kO.js → chunk-PQ6SQG4A-TF58UVMU.js} +1 -1
  31. package/dist/web/assets/{chunk-PU5JKC2W-C7Gry6md.js → chunk-PU5JKC2W-ek7k4QVB.js} +1 -1
  32. package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +1 -0
  33. package/dist/web/assets/{chunk-R5LLSJPH-CMY0PkRK.js → chunk-R5LLSJPH-CFwSJijQ.js} +1 -1
  34. package/dist/web/assets/{chunk-WL4C6EOR-CXuQvlyu.js → chunk-WL4C6EOR-ByUrSRin.js} +1 -1
  35. package/dist/web/assets/{chunk-XIRO2GV7-DRJEb7Zb.js → chunk-XIRO2GV7-Djlmrely.js} +1 -1
  36. package/dist/web/assets/{chunk-XPW4576I-BPEX8KhL.js → chunk-XPW4576I-BPQQBakK.js} +1 -1
  37. package/dist/web/assets/{chunk-XZSTWKYB-Cb0iqycX.js → chunk-XZSTWKYB-DxAOx4hG.js} +1 -1
  38. package/dist/web/assets/{chunk-YBOYWFTD-av5aeHLq.js → chunk-YBOYWFTD-rQG3QH5s.js} +1 -1
  39. package/dist/web/assets/classDiagram-VBA2DB6C-BA8Nj-_C.js +1 -0
  40. package/dist/web/assets/classDiagram-v2-RAHNMMFH-DjYu-6mn.js +1 -0
  41. package/dist/web/assets/clone-LRxlvnMj.js +1 -0
  42. package/dist/web/assets/code-editor-BixOXePn.js +8 -0
  43. package/dist/web/assets/{cose-bilkent-S5V4N54A-qudEiMCT.js → cose-bilkent-S5V4N54A-B_AWZsOP.js} +1 -1
  44. package/dist/web/assets/csv-parser-CNNw2RVA.js +6 -0
  45. package/dist/web/assets/{csv-preview-sx6DC51G.js → csv-preview-D2pJJj3K.js} +3 -8
  46. package/dist/web/assets/{dagre-BFcnKyBF.js → dagre-DHq9bhnd.js} +1 -1
  47. package/dist/web/assets/{dagre-KLK3FWXG-C3O-MTLf.js → dagre-KLK3FWXG-BdJr7Byp.js} +1 -1
  48. package/dist/web/assets/database-viewer-DfLe8ewt.js +2 -0
  49. package/dist/web/assets/{diagram-E7M64L7V-DxPjK7_c.js → diagram-E7M64L7V-_db4pBVA.js} +1 -1
  50. package/dist/web/assets/{diagram-IFDJBPK2-sqTog_XV.js → diagram-IFDJBPK2-xKoeuiJx.js} +1 -1
  51. package/dist/web/assets/{diagram-P4PSJMXO-hzmp0GHK.js → diagram-P4PSJMXO-C8tjJsev.js} +1 -1
  52. package/dist/web/assets/diff-viewer-eFO08m_L.js +4 -0
  53. package/dist/web/assets/dist-DIV6WgAG.js +41 -0
  54. package/dist/web/assets/{erDiagram-INFDFZHY-DLeYhAAT.js → erDiagram-INFDFZHY-BSh2z9Df.js} +1 -1
  55. package/dist/web/assets/{extension-webview-2XjXXtyy.js → extension-webview-4CL9kCKR.js} +1 -1
  56. package/dist/web/assets/{flowDiagram-PKNHOUZH-CRxlE9Sr.js → flowDiagram-PKNHOUZH-oYaovqyp.js} +1 -1
  57. package/dist/web/assets/{ganttDiagram-A5KZAMGK-BdjmoMLS.js → ganttDiagram-A5KZAMGK-DmL26q2P.js} +1 -1
  58. package/dist/web/assets/git-graph-BqeE_o17.js +1 -0
  59. package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +1 -0
  60. package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-BeHSX7kk.js → gitGraphDiagram-K3NZZRJ6-CMoukSrY.js} +1 -1
  61. package/dist/web/assets/{graphlib-Duh_bWLa.js → graphlib-BcsNnGcW.js} +1 -1
  62. package/dist/web/assets/index-BWLy2h18.css +2 -0
  63. package/dist/web/assets/index-DwrCg0TN.js +30 -0
  64. package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +1 -0
  65. package/dist/web/assets/infoDiagram-LFFYTUFH-DWwumDkq.js +2 -0
  66. package/dist/web/assets/{isEmpty-B9L-Ge-H.js → isEmpty-bnrF3Qbc.js} +1 -1
  67. package/dist/web/assets/{ishikawaDiagram-PHBUUO56-Cu0Rt1Ok.js → ishikawaDiagram-PHBUUO56-D05_LyL7.js} +1 -1
  68. package/dist/web/assets/{journeyDiagram-4ABVD52K-CgDI-UG4.js → journeyDiagram-4ABVD52K-B_L20qMe.js} +1 -1
  69. package/dist/web/assets/{kanban-definition-K7BYSVSG-h4g10UHL.js → kanban-definition-K7BYSVSG-CZ535BbZ.js} +1 -1
  70. package/dist/web/assets/keybindings-store-BNBONtSd.js +1 -0
  71. package/dist/web/assets/{line-B75-Rx70.js → line-CVvo3dRu.js} +1 -1
  72. package/dist/web/assets/{linear-Bcjv9FQt.js → linear-DP4mkX3m.js} +1 -1
  73. package/dist/web/assets/{markdown-renderer-Bb7OSpxF.js → markdown-renderer-BUqab2os.js} +5 -5
  74. package/dist/web/assets/{mermaid-parser.core-8u2leTXI.js → mermaid-parser.core-C7UwoIh6.js} +2 -2
  75. package/dist/web/assets/{mindmap-definition-YRQLILUH-BaOBwb-W.js → mindmap-definition-YRQLILUH-x0MTutJp.js} +1 -1
  76. package/dist/web/assets/{ordinal-LFEjVtwQ.js → ordinal-_K3x1fkz.js} +1 -1
  77. package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +1 -0
  78. package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +1 -0
  79. package/dist/web/assets/{pieDiagram-SKSYHLDU-At5Kz0KK.js → pieDiagram-SKSYHLDU-C1Gjrtzy.js} +1 -1
  80. package/dist/web/assets/{port-forwarding-tab-bD8MKumH.js → port-forwarding-tab-CfO-UJ84.js} +1 -1
  81. package/dist/web/assets/postgres-viewer-BVJZ44eU.js +13 -0
  82. package/dist/web/assets/{quadrantDiagram-337W2JSQ-CdjGIDfw.js → quadrantDiagram-337W2JSQ-C8bzJCjQ.js} +1 -1
  83. package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +1 -0
  84. package/dist/web/assets/{requirementDiagram-Z7DCOOCP-B9F_Cx_p.js → requirementDiagram-Z7DCOOCP-pQyah6WB.js} +1 -1
  85. package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-RolPi8bU.js → sankeyDiagram-WA2Y5GQK-T6RgG-N8.js} +1 -1
  86. package/dist/web/assets/{sequenceDiagram-2WXFIKYE-DM-tMAhx.js → sequenceDiagram-2WXFIKYE-BQDJ4CVs.js} +1 -1
  87. package/dist/web/assets/settings-tab-C6hdJujW.js +1 -0
  88. package/dist/web/assets/sql-completion-provider-DM9Qov6L.js +1 -0
  89. package/dist/web/assets/sql-query-editor-OhZa4Z9F.js +3 -0
  90. package/dist/web/assets/sqlite-viewer-C8p1_jz4.js +1 -0
  91. package/dist/web/assets/{stateDiagram-RAJIS63D-C4EMl6jf.js → stateDiagram-RAJIS63D-66vhiIuk.js} +1 -1
  92. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-BGVqj_g9.js +1 -0
  93. package/dist/web/assets/{terminal-tab-Cq6vQ9W9.js → terminal-tab-CaO0WnIo.js} +2 -2
  94. package/dist/web/assets/text-wrap-BWNOVswA.js +1 -0
  95. package/dist/web/assets/{timeline-definition-YZTLITO2-A4PN_Efm.js → timeline-definition-YZTLITO2-DwZqB3nn.js} +1 -1
  96. package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +1 -0
  97. package/dist/web/assets/use-monaco-theme-U9ZhfvHB.js +11 -0
  98. package/dist/web/assets/{vennDiagram-LZ73GAT5-ywK7LMaH.js → vennDiagram-LZ73GAT5-s9Z71fz-.js} +1 -1
  99. package/dist/web/assets/x-D2_KzIET.js +1 -0
  100. package/dist/web/assets/{xychartDiagram-JWTSCODW-DylHYNtJ.js → xychartDiagram-JWTSCODW-DRa_TH4B.js} +1 -1
  101. package/dist/web/index.html +9 -8
  102. package/dist/web/sw.js +1 -1
  103. package/package.json +1 -1
  104. package/src/cli/commands/bot-cmd.ts +4 -0
  105. package/src/cli/commands/db-cmd.ts +4 -3
  106. package/src/server/routes/database.ts +126 -9
  107. package/src/services/cloud.service.ts +2 -1
  108. package/src/services/database/sqlite-adapter.ts +1 -0
  109. package/src/services/db.service.ts +42 -3
  110. package/src/services/postgres.service.ts +37 -6
  111. package/src/services/sqlite.service.ts +18 -4
  112. package/src/services/table-cache.service.ts +2 -3
  113. package/src/types/database.ts +2 -0
  114. package/src/web/components/database/connection-form-dialog.tsx +17 -8
  115. package/src/web/components/database/connection-list.tsx +191 -139
  116. package/src/web/components/database/data-grid.tsx +634 -0
  117. package/src/web/components/database/database-sidebar.tsx +4 -1
  118. package/src/web/components/database/database-viewer.tsx +204 -225
  119. package/src/web/components/database/export-button.tsx +100 -0
  120. package/src/web/components/database/sql-completion-provider.ts +301 -0
  121. package/src/web/components/database/sql-query-editor.tsx +123 -0
  122. package/src/web/components/database/use-connections.ts +21 -1
  123. package/src/web/components/database/use-database.ts +59 -7
  124. package/src/web/components/editor/code-editor.tsx +224 -16
  125. package/src/web/components/sqlite/sqlite-query-editor.tsx +3 -90
  126. package/src/web/components/sqlite/sqlite-viewer.tsx +0 -2
  127. package/src/web/components/sqlite/use-sqlite.ts +1 -1
  128. package/dist/web/assets/architecture-PBZL5I3N-CFzkFKEL.js +0 -1
  129. package/dist/web/assets/channel-C2fMafck.js +0 -1
  130. package/dist/web/assets/chat-tab-CfdMDCBK.js +0 -10
  131. package/dist/web/assets/chunk-GLR3WWYH-DBdWQ3zy.js +0 -2
  132. package/dist/web/assets/chunk-HHEYEP7N-BBw_z0fW.js +0 -1
  133. package/dist/web/assets/chunk-QZHKN3VN-DFKFM_C1.js +0 -1
  134. package/dist/web/assets/classDiagram-VBA2DB6C-Dp4Kk3Yb.js +0 -1
  135. package/dist/web/assets/classDiagram-v2-RAHNMMFH-D8IvcV_B.js +0 -1
  136. package/dist/web/assets/clone-B2hUek6n.js +0 -1
  137. package/dist/web/assets/code-editor-DhCfJIpG.js +0 -2
  138. package/dist/web/assets/database-viewer-DLkAUBpm.js +0 -1
  139. package/dist/web/assets/diff-viewer-DWVWsekJ.js +0 -4
  140. package/dist/web/assets/dist-C40JmyoH.js +0 -13
  141. package/dist/web/assets/dist-DRTW9IWi.js +0 -41
  142. package/dist/web/assets/git-graph-Ds5bs1cM.js +0 -1
  143. package/dist/web/assets/gitGraph-HDMCJU4V-CNlas3Rz.js +0 -1
  144. package/dist/web/assets/index-8b0LM6IC.js +0 -30
  145. package/dist/web/assets/index-BnMECpW3.css +0 -2
  146. package/dist/web/assets/info-3K5VOQVL-BDzTLc11.js +0 -1
  147. package/dist/web/assets/infoDiagram-LFFYTUFH-ZZmpgc6t.js +0 -2
  148. package/dist/web/assets/keybindings-store-C06Z0Zhk.js +0 -1
  149. package/dist/web/assets/packet-RMMSAZCW-IVa5F-go.js +0 -1
  150. package/dist/web/assets/pie-UPGHQEXC-CvXHKAzp.js +0 -1
  151. package/dist/web/assets/postgres-viewer-C3OND65T.js +0 -1
  152. package/dist/web/assets/radar-KQ55EAFF-Z-Tr5wtS.js +0 -1
  153. package/dist/web/assets/settings-tab-cuMIkUNV.js +0 -1
  154. package/dist/web/assets/sqlite-viewer-CpjnwDtk.js +0 -1
  155. package/dist/web/assets/stateDiagram-v2-FVOUBMTO-B-UjZch3.js +0 -1
  156. package/dist/web/assets/treemap-KZPCXAKY-C9TYRE0k.js +0 -1
  157. package/dist/web/assets/use-monaco-theme-BH9sQ-Yu.js +0 -11
  158. /package/dist/web/assets/{api-client-BKIT_Qeg.js → api-client-BfBM3I7n.js} +0 -0
  159. /package/dist/web/assets/{array-DqLCdDFv.js → array-B9UHiPd-.js} +0 -0
  160. /package/dist/web/assets/{chevron-right-5HgK6l7K.js → chevron-right-4zq1jPv6.js} +0 -0
  161. /package/dist/web/assets/{columns-2-cEVJHYd7.js → columns-2-BoZAN-iw.js} +0 -0
  162. /package/dist/web/assets/{cytoscape.esm-CWPXKqbJ.js → cytoscape.esm-BW-DbntU.js} +0 -0
  163. /package/dist/web/assets/{defaultLocale-CrJzLgRD.js → defaultLocale-5eAKkKJC.js} +0 -0
  164. /package/dist/web/assets/{dist-Cep75xXf.js → dist-CSJdAyA9.js} +0 -0
  165. /package/dist/web/assets/{init-C0r9Gk5G.js → init-DlZdxViB.js} +0 -0
  166. /package/dist/web/assets/{isArrayLikeObject-CGBoxvCD.js → isArrayLikeObject-B_v2FtYn.js} +0 -0
  167. /package/dist/web/assets/{katex-DzXRfQ_m.js → katex-Bqvo_ZG0.js} +0 -0
  168. /package/dist/web/assets/{lib-mag4ySk-.js → lib-DurwGtQO.js} +0 -0
  169. /package/dist/web/assets/{math-y9zN1W-N.js → math-069Z4SuC.js} +0 -0
  170. /package/dist/web/assets/{path-DIKpVbHL.js → path-6uRLdFF7.js} +0 -0
  171. /package/dist/web/assets/{rough.esm-nHaDi0Kw.js → rough.esm-JX0wREDd.js} +0 -0
  172. /package/dist/web/assets/{src-Dw4QhedI.js → src-BqX54PbV.js} +0 -0
  173. /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
- const isExpanded = expandedConns.has(conn.id);
98
- const tables = cachedTables.get(conn.id) ?? [];
99
- const isRefreshing = refreshingIds.has(conn.id);
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
- {/* DB type badge */}
138
- <span className="shrink-0 text-[9px] text-text-subtle uppercase px-1 rounded bg-surface-elevated">
139
- {conn.type === "postgres" ? "PG" : "DB"}
140
- </span>
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
- {/* Readonly lock */}
143
- {conn.readonly === 1 && (
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
- {/* Actions (hover) */}
150
- <div className="flex can-hover:hidden can-hover:group-hover:flex items-center gap-0.5 shrink-0">
151
- <button
152
- onClick={() => handleRefresh(conn.id)}
153
- disabled={isRefreshing}
154
- className="p-0.5 text-text-subtle hover:text-foreground transition-colors"
155
- title="Refresh tables"
156
- >
157
- <RefreshCw className={cn("size-3", isRefreshing && "animate-spin")} />
158
- </button>
159
- <button
160
- onClick={() => onEdit(conn)}
161
- className="p-0.5 text-text-subtle hover:text-foreground transition-colors"
162
- title="Edit"
163
- >
164
- <Pencil className="size-3" />
165
- </button>
166
- <button
167
- onClick={() => onDelete(conn.id)}
168
- className="p-0.5 text-text-subtle hover:text-red-500 transition-colors"
169
- title="Delete"
170
- >
171
- <Trash2 className="size-3" />
172
- </button>
173
- </div>
174
- </div>
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
- {/* Table list (expanded) with tree guide line */}
177
- {isExpanded && (
178
- <div className="ml-[11px] border-l border-dashed border-border pl-3">
179
- {isRefreshing && tables.length === 0 && (
180
- <p className="text-[10px] text-text-subtle px-2 py-1">Loading…</p>
181
- )}
182
- {!isRefreshing && tables.length === 0 && (
183
- <p className="text-[10px] text-text-subtle px-2 py-1">No tables cached</p>
184
- )}
185
- {tables.length > 0 && (
186
- <TableListWithFilter
187
- connId={conn.id}
188
- tables={tables}
189
- filter={tableFilter.get(conn.id) ?? ""}
190
- onFilterChange={(v) => setTableFilter((prev) => new Map(prev).set(conn.id, v))}
191
- onOpenTable={(tableName, schemaName) => onOpenTable(conn, tableName, schemaName)}
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
- </div>
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 list with filter ---------- */
209
- const MAX_TABLE_HEIGHT = 200; // px
226
+ /* ---------- Schema > Table > Column tree ---------- */
210
227
 
211
- function TableListWithFilter({ connId, tables, filter, onFilterChange, onOpenTable }: {
228
+ function SchemaTableTree({ connId, connType, schemas, isSingleSchema, filter, expandedTables, loadingColumns, columnCache, onToggleTable, onOpenTable }: {
212
229
  connId: number;
213
- tables: CachedTable[];
230
+ connType: "sqlite" | "postgres";
231
+ schemas: Map<string, CachedTable[]>;
232
+ isSingleSchema: boolean;
214
233
  filter: string;
215
- onFilterChange: (v: string) => void;
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 filtered = useMemo(() => {
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
- {/* Filter input — show when many tables */}
227
- {tables.length > 5 && (
228
- <div className="flex items-center gap-1 px-1 py-0.5">
229
- <Search className="size-2.5 text-text-subtle shrink-0" />
230
- <input
231
- type="text"
232
- value={filter}
233
- onChange={(e) => onFilterChange(e.target.value)}
234
- placeholder="Filter tables…"
235
- className="w-full text-[10px] bg-transparent border-none outline-none text-foreground placeholder:text-text-subtle"
236
- />
237
- </div>
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
  }