@adecore/database 0.0.1 → 0.16.0-beta.0

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 (400) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -1
  3. package/dist/DatabaseProvider.d.ts +13 -0
  4. package/dist/DatabaseProvider.js +17 -0
  5. package/dist/actions.d.ts +40 -0
  6. package/dist/actions.js +1 -0
  7. package/dist/client/client.d.ts +5 -0
  8. package/dist/client/client.js +245 -0
  9. package/dist/client/index.d.ts +2 -0
  10. package/dist/client/index.js +2 -0
  11. package/dist/client/types.d.ts +97 -0
  12. package/dist/client/types.js +13 -0
  13. package/dist/client-context.d.ts +16 -0
  14. package/dist/client-context.js +17 -0
  15. package/dist/connections/ConnectionForm.d.ts +10 -0
  16. package/dist/connections/ConnectionForm.js +145 -0
  17. package/dist/connections/ConnectionManager.d.ts +12 -0
  18. package/dist/connections/ConnectionManager.js +93 -0
  19. package/dist/connections/EngineIcon.d.ts +9 -0
  20. package/dist/connections/EngineIcon.js +7 -0
  21. package/dist/connections/connection-config.d.ts +16 -0
  22. package/dist/connections/connection-config.js +107 -0
  23. package/dist/connections/docker.d.ts +9 -0
  24. package/dist/connections/docker.js +38 -0
  25. package/dist/connections/engine-icons.d.ts +3 -0
  26. package/dist/connections/engine-icons.js +3 -0
  27. package/dist/connections/use-docker-containers.d.ts +17 -0
  28. package/dist/connections/use-docker-containers.js +26 -0
  29. package/dist/console/DestructiveDialog.d.ts +9 -0
  30. package/dist/console/DestructiveDialog.js +9 -0
  31. package/dist/console/HistoryPanel.d.ts +10 -0
  32. package/dist/console/HistoryPanel.js +15 -0
  33. package/dist/console/QueryConsole.d.ts +14 -0
  34. package/dist/console/QueryConsole.js +261 -0
  35. package/dist/console/SchemaPicker.d.ts +11 -0
  36. package/dist/console/SchemaPicker.js +8 -0
  37. package/dist/console/SqlEditor.d.ts +12 -0
  38. package/dist/console/SqlEditor.js +61 -0
  39. package/dist/console/StatementResultView.d.ts +18 -0
  40. package/dist/console/StatementResultView.js +32 -0
  41. package/dist/console/TransactionControls.d.ts +13 -0
  42. package/dist/console/TransactionControls.js +12 -0
  43. package/dist/console/destructive.d.ts +7 -0
  44. package/dist/console/destructive.js +43 -0
  45. package/dist/console/editing.d.ts +12 -0
  46. package/dist/console/editing.js +60 -0
  47. package/dist/console/history.d.ts +13 -0
  48. package/dist/console/history.js +43 -0
  49. package/dist/console/labels.d.ts +1 -0
  50. package/dist/console/labels.js +6 -0
  51. package/dist/console/outcome.d.ts +6 -0
  52. package/dist/console/outcome.js +11 -0
  53. package/dist/console/schemas.d.ts +2 -0
  54. package/dist/console/schemas.js +5 -0
  55. package/dist/console/shortcuts.d.ts +2 -0
  56. package/dist/console/shortcuts.js +3 -0
  57. package/dist/console/useConsoleHistory.d.ts +7 -0
  58. package/dist/console/useConsoleHistory.js +22 -0
  59. package/dist/ddl/alter-mysql.d.ts +4 -0
  60. package/dist/ddl/alter-mysql.js +79 -0
  61. package/dist/ddl/alter-sqlite.d.ts +4 -0
  62. package/dist/ddl/alter-sqlite.js +80 -0
  63. package/dist/ddl/alter.d.ts +4 -0
  64. package/dist/ddl/alter.js +24 -0
  65. package/dist/ddl/column.d.ts +9 -0
  66. package/dist/ddl/column.js +62 -0
  67. package/dist/ddl/create.d.ts +12 -0
  68. package/dist/ddl/create.js +74 -0
  69. package/dist/ddl/dialect.d.ts +12 -0
  70. package/dist/ddl/dialect.js +34 -0
  71. package/dist/ddl/diff.d.ts +34 -0
  72. package/dist/ddl/diff.js +111 -0
  73. package/dist/ddl/draft.d.ts +46 -0
  74. package/dist/ddl/draft.js +70 -0
  75. package/dist/ddl/index.d.ts +10 -0
  76. package/dist/ddl/index.js +10 -0
  77. package/dist/ddl/parse.d.ts +17 -0
  78. package/dist/ddl/parse.js +131 -0
  79. package/dist/ddl/statements.d.ts +6 -0
  80. package/dist/ddl/statements.js +12 -0
  81. package/dist/ddl/types.d.ts +6 -0
  82. package/dist/ddl/types.js +52 -0
  83. package/dist/ddl/validate.d.ts +7 -0
  84. package/dist/ddl/validate.js +38 -0
  85. package/dist/designer/ColumnsEditor.d.ts +2 -0
  86. package/dist/designer/ColumnsEditor.js +24 -0
  87. package/dist/designer/DesignerView.d.ts +25 -0
  88. package/dist/designer/DesignerView.js +33 -0
  89. package/dist/designer/ForeignKeysEditor.d.ts +7 -0
  90. package/dist/designer/ForeignKeysEditor.js +24 -0
  91. package/dist/designer/IndexesEditor.d.ts +2 -0
  92. package/dist/designer/IndexesEditor.js +13 -0
  93. package/dist/designer/NamePicker.d.ts +11 -0
  94. package/dist/designer/NamePicker.js +13 -0
  95. package/dist/designer/OptionsEditor.d.ts +2 -0
  96. package/dist/designer/OptionsEditor.js +17 -0
  97. package/dist/designer/SqlPreview.d.ts +12 -0
  98. package/dist/designer/SqlPreview.js +17 -0
  99. package/dist/designer/TableDesigner.d.ts +10 -0
  100. package/dist/designer/TableDesigner.js +103 -0
  101. package/dist/designer/edit.d.ts +17 -0
  102. package/dist/designer/edit.js +115 -0
  103. package/dist/designer/editor-props.d.ts +10 -0
  104. package/dist/designer/editor-props.js +1 -0
  105. package/dist/designer/plan.d.ts +9 -0
  106. package/dist/designer/plan.js +13 -0
  107. package/dist/designer/recover.d.ts +2 -0
  108. package/dist/designer/recover.js +18 -0
  109. package/dist/designer/useReferenceColumns.d.ts +3 -0
  110. package/dist/designer/useReferenceColumns.js +36 -0
  111. package/dist/explorer/DatabaseExplorer.d.ts +13 -0
  112. package/dist/explorer/DatabaseExplorer.js +264 -0
  113. package/dist/explorer/RowMenu.d.ts +11 -0
  114. package/dist/explorer/RowMenu.js +89 -0
  115. package/dist/explorer/TableDialog.d.ts +15 -0
  116. package/dist/explorer/TableDialog.js +50 -0
  117. package/dist/explorer/expansion.d.ts +7 -0
  118. package/dist/explorer/expansion.js +31 -0
  119. package/dist/explorer/table-statements.d.ts +5 -0
  120. package/dist/explorer/table-statements.js +12 -0
  121. package/dist/explorer/tree.d.ts +125 -0
  122. package/dist/explorer/tree.js +295 -0
  123. package/dist/explorer/use-explorer-loads.d.ts +12 -0
  124. package/dist/explorer/use-explorer-loads.js +109 -0
  125. package/dist/explorer/use-stored-expansion.d.ts +9 -0
  126. package/dist/explorer/use-stored-expansion.js +47 -0
  127. package/dist/grid/CellEditor.d.ts +9 -0
  128. package/dist/grid/CellEditor.js +29 -0
  129. package/dist/grid/CopyAsMenu.d.ts +7 -0
  130. package/dist/grid/CopyAsMenu.js +10 -0
  131. package/dist/grid/DataGrid.d.ts +36 -0
  132. package/dist/grid/DataGrid.js +512 -0
  133. package/dist/grid/EnumPicker.d.ts +17 -0
  134. package/dist/grid/EnumPicker.js +53 -0
  135. package/dist/grid/GridHeaderCell.d.ts +36 -0
  136. package/dist/grid/GridHeaderCell.js +69 -0
  137. package/dist/grid/RecordView.d.ts +20 -0
  138. package/dist/grid/RecordView.js +126 -0
  139. package/dist/grid/ValueDock.d.ts +11 -0
  140. package/dist/grid/ValueDock.js +28 -0
  141. package/dist/grid/aggregates.d.ts +22 -0
  142. package/dist/grid/aggregates.js +62 -0
  143. package/dist/grid/cell-range.d.ts +14 -0
  144. package/dist/grid/cell-range.js +11 -0
  145. package/dist/grid/column-layout.d.ts +18 -0
  146. package/dist/grid/column-layout.js +49 -0
  147. package/dist/grid/column-selection.d.ts +11 -0
  148. package/dist/grid/column-selection.js +28 -0
  149. package/dist/grid/column-view.d.ts +10 -0
  150. package/dist/grid/column-view.js +23 -0
  151. package/dist/grid/copy-formats.d.ts +15 -0
  152. package/dist/grid/copy-formats.js +80 -0
  153. package/dist/grid/display.d.ts +20 -0
  154. package/dist/grid/display.js +81 -0
  155. package/dist/grid/edit-value.d.ts +5 -0
  156. package/dist/grid/edit-value.js +55 -0
  157. package/dist/grid/enum-type.d.ts +7 -0
  158. package/dist/grid/enum-type.js +52 -0
  159. package/dist/grid/focused-value.d.ts +4 -0
  160. package/dist/grid/focused-value.js +19 -0
  161. package/dist/grid/layout.d.ts +22 -0
  162. package/dist/grid/layout.js +74 -0
  163. package/dist/grid/navigation.d.ts +15 -0
  164. package/dist/grid/navigation.js +52 -0
  165. package/dist/grid/row-selection.d.ts +9 -0
  166. package/dist/grid/row-selection.js +24 -0
  167. package/dist/grid/sort.d.ts +7 -0
  168. package/dist/grid/sort.js +7 -0
  169. package/dist/grid/types.d.ts +30 -0
  170. package/dist/grid/types.js +9 -0
  171. package/dist/host/helper-path.d.ts +7 -0
  172. package/dist/host/helper-path.js +15 -0
  173. package/dist/host/host.d.ts +2 -0
  174. package/dist/host/host.js +343 -0
  175. package/dist/host/index.d.ts +5 -0
  176. package/dist/host/index.js +4 -0
  177. package/dist/host/spawn.d.ts +7 -0
  178. package/dist/host/spawn.js +98 -0
  179. package/dist/host/types.d.ts +19 -0
  180. package/dist/host/types.js +1 -0
  181. package/dist/host/validate.d.ts +10 -0
  182. package/dist/host/validate.js +339 -0
  183. package/dist/index.d.ts +14 -0
  184. package/dist/index.js +13 -0
  185. package/dist/locales/en.json +577 -0
  186. package/dist/locales/nl.json +577 -0
  187. package/dist/locales.d.ts +4 -0
  188. package/dist/locales.js +14 -0
  189. package/dist/protocol/connection.d.ts +56 -0
  190. package/dist/protocol/connection.js +1 -0
  191. package/dist/protocol/errors.d.ts +7 -0
  192. package/dist/protocol/errors.js +1 -0
  193. package/dist/protocol/index.d.ts +5 -0
  194. package/dist/protocol/index.js +3 -0
  195. package/dist/protocol/messages.d.ts +250 -0
  196. package/dist/protocol/messages.js +2 -0
  197. package/dist/protocol/schema.d.ts +53 -0
  198. package/dist/protocol/schema.js +1 -0
  199. package/dist/protocol/values.d.ts +21 -0
  200. package/dist/protocol/values.js +15 -0
  201. package/dist/sql-split.d.ts +15 -0
  202. package/dist/sql-split.js +201 -0
  203. package/dist/sql.d.ts +29 -0
  204. package/dist/sql.js +104 -0
  205. package/dist/structure/StructureView.d.ts +14 -0
  206. package/dist/structure/StructureView.js +99 -0
  207. package/dist/structure/structure-text.d.ts +2 -0
  208. package/dist/structure/structure-text.js +2 -0
  209. package/dist/table/CommandField.d.ts +14 -0
  210. package/dist/table/CommandField.js +157 -0
  211. package/dist/table/ImportDialog.d.ts +27 -0
  212. package/dist/table/ImportDialog.js +28 -0
  213. package/dist/table/TableStatusBar.d.ts +19 -0
  214. package/dist/table/TableStatusBar.js +22 -0
  215. package/dist/table/TableToolbar.d.ts +43 -0
  216. package/dist/table/TableToolbar.js +16 -0
  217. package/dist/table/TableView.d.ts +13 -0
  218. package/dist/table/TableView.js +508 -0
  219. package/dist/table/command-field.d.ts +57 -0
  220. package/dist/table/command-field.js +203 -0
  221. package/dist/table/grid-rows.d.ts +23 -0
  222. package/dist/table/grid-rows.js +55 -0
  223. package/dist/table/import-mapping.d.ts +7 -0
  224. package/dist/table/import-mapping.js +28 -0
  225. package/dist/table/layout-store.d.ts +13 -0
  226. package/dist/table/layout-store.js +43 -0
  227. package/dist/table/paging.d.ts +12 -0
  228. package/dist/table/paging.js +20 -0
  229. package/dist/table/pending.d.ts +32 -0
  230. package/dist/table/pending.js +94 -0
  231. package/dist/table/references.d.ts +12 -0
  232. package/dist/table/references.js +21 -0
  233. package/dist/table/useLoaded.d.ts +7 -0
  234. package/dist/table/useLoaded.js +36 -0
  235. package/dist/table/useStoredLayout.d.ts +8 -0
  236. package/dist/table/useStoredLayout.js +39 -0
  237. package/dist/table/useTableTransfer.d.ts +51 -0
  238. package/dist/table/useTableTransfer.js +125 -0
  239. package/dist/testing/fake.d.ts +21 -0
  240. package/dist/testing/fake.js +372 -0
  241. package/dist/testing/index.d.ts +1 -0
  242. package/dist/testing/index.js +1 -0
  243. package/dist/use-popup-press.d.ts +1 -0
  244. package/dist/use-popup-press.js +24 -0
  245. package/dist/use-schema-change.d.ts +1 -0
  246. package/dist/use-schema-change.js +13 -0
  247. package/dist/use-stable-callback.d.ts +1 -0
  248. package/dist/use-stable-callback.js +9 -0
  249. package/dist/value/ValuePanel.d.ts +14 -0
  250. package/dist/value/ValuePanel.js +103 -0
  251. package/dist/value/draft.d.ts +33 -0
  252. package/dist/value/draft.js +44 -0
  253. package/dist/value/hex.d.ts +14 -0
  254. package/dist/value/hex.js +46 -0
  255. package/dist/value/json.d.ts +9 -0
  256. package/dist/value/json.js +71 -0
  257. package/dist/value/views.d.ts +22 -0
  258. package/dist/value/views.js +54 -0
  259. package/dist/workbench/DatabaseWorkbench.d.ts +9 -0
  260. package/dist/workbench/DatabaseWorkbench.js +179 -0
  261. package/dist/workbench/DesignerTab.d.ts +10 -0
  262. package/dist/workbench/DesignerTab.js +22 -0
  263. package/dist/workbench/TableTab.d.ts +10 -0
  264. package/dist/workbench/TableTab.js +8 -0
  265. package/dist/workbench/tabs.d.ts +42 -0
  266. package/dist/workbench/tabs.js +139 -0
  267. package/package.json +79 -3
  268. package/src/DatabaseProvider.tsx +37 -0
  269. package/src/actions.ts +34 -0
  270. package/src/client/client.ts +324 -0
  271. package/src/client/index.ts +18 -0
  272. package/src/client/types.ts +161 -0
  273. package/src/client-context.ts +35 -0
  274. package/src/connections/ConnectionForm.tsx +474 -0
  275. package/src/connections/ConnectionManager.tsx +224 -0
  276. package/src/connections/EngineIcon.tsx +18 -0
  277. package/src/connections/connection-config.ts +143 -0
  278. package/src/connections/docker.ts +50 -0
  279. package/src/connections/engine-icons.ts +5 -0
  280. package/src/connections/use-docker-containers.ts +41 -0
  281. package/src/console/DestructiveDialog.tsx +40 -0
  282. package/src/console/HistoryPanel.tsx +67 -0
  283. package/src/console/QueryConsole.tsx +439 -0
  284. package/src/console/SchemaPicker.tsx +32 -0
  285. package/src/console/SqlEditor.tsx +95 -0
  286. package/src/console/StatementResultView.tsx +176 -0
  287. package/src/console/TransactionControls.tsx +53 -0
  288. package/src/console/destructive.ts +60 -0
  289. package/src/console/editing.ts +77 -0
  290. package/src/console/history.ts +62 -0
  291. package/src/console/labels.ts +7 -0
  292. package/src/console/outcome.ts +14 -0
  293. package/src/console/schemas.ts +7 -0
  294. package/src/console/shortcuts.ts +4 -0
  295. package/src/console/useConsoleHistory.ts +39 -0
  296. package/src/ddl/alter-mysql.ts +90 -0
  297. package/src/ddl/alter-sqlite.ts +93 -0
  298. package/src/ddl/alter.ts +29 -0
  299. package/src/ddl/column.ts +74 -0
  300. package/src/ddl/create.ts +92 -0
  301. package/src/ddl/dialect.ts +53 -0
  302. package/src/ddl/diff.ts +164 -0
  303. package/src/ddl/draft.ts +136 -0
  304. package/src/ddl/index.ts +10 -0
  305. package/src/ddl/parse.ts +161 -0
  306. package/src/ddl/statements.ts +20 -0
  307. package/src/ddl/types.ts +60 -0
  308. package/src/ddl/validate.ts +47 -0
  309. package/src/designer/ColumnsEditor.tsx +204 -0
  310. package/src/designer/DesignerView.tsx +184 -0
  311. package/src/designer/ForeignKeysEditor.tsx +132 -0
  312. package/src/designer/IndexesEditor.tsx +63 -0
  313. package/src/designer/NamePicker.tsx +44 -0
  314. package/src/designer/OptionsEditor.tsx +101 -0
  315. package/src/designer/SqlPreview.tsx +56 -0
  316. package/src/designer/TableDesigner.tsx +186 -0
  317. package/src/designer/edit.ts +137 -0
  318. package/src/designer/editor-props.ts +13 -0
  319. package/src/designer/plan.ts +25 -0
  320. package/src/designer/recover.ts +20 -0
  321. package/src/designer/useReferenceColumns.ts +54 -0
  322. package/src/explorer/DatabaseExplorer.tsx +450 -0
  323. package/src/explorer/RowMenu.tsx +134 -0
  324. package/src/explorer/TableDialog.tsx +80 -0
  325. package/src/explorer/expansion.ts +47 -0
  326. package/src/explorer/table-statements.ts +18 -0
  327. package/src/explorer/tree.ts +453 -0
  328. package/src/explorer/use-explorer-loads.ts +157 -0
  329. package/src/explorer/use-stored-expansion.ts +66 -0
  330. package/src/grid/CellEditor.tsx +53 -0
  331. package/src/grid/CopyAsMenu.tsx +33 -0
  332. package/src/grid/DataGrid.tsx +871 -0
  333. package/src/grid/EnumPicker.tsx +108 -0
  334. package/src/grid/GridHeaderCell.tsx +243 -0
  335. package/src/grid/RecordView.tsx +293 -0
  336. package/src/grid/ValueDock.tsx +52 -0
  337. package/src/grid/aggregates.ts +94 -0
  338. package/src/grid/cell-range.ts +26 -0
  339. package/src/grid/column-layout.ts +71 -0
  340. package/src/grid/column-selection.ts +42 -0
  341. package/src/grid/column-view.ts +35 -0
  342. package/src/grid/copy-formats.ts +108 -0
  343. package/src/grid/display.ts +109 -0
  344. package/src/grid/edit-value.ts +60 -0
  345. package/src/grid/enum-type.ts +62 -0
  346. package/src/grid/focused-value.ts +23 -0
  347. package/src/grid/layout.ts +91 -0
  348. package/src/grid/navigation.ts +74 -0
  349. package/src/grid/row-selection.ts +35 -0
  350. package/src/grid/sort.ts +12 -0
  351. package/src/grid/types.ts +59 -0
  352. package/src/host/helper-path.ts +41 -0
  353. package/src/host/host.ts +456 -0
  354. package/src/host/index.ts +5 -0
  355. package/src/host/spawn.ts +124 -0
  356. package/src/host/types.ts +46 -0
  357. package/src/host/validate.ts +402 -0
  358. package/src/index.ts +37 -0
  359. package/src/locales/en.json +577 -0
  360. package/src/locales/nl.json +577 -0
  361. package/src/locales.ts +18 -0
  362. package/src/protocol/connection.ts +86 -0
  363. package/src/protocol/errors.ts +38 -0
  364. package/src/protocol/index.ts +33 -0
  365. package/src/protocol/messages.ts +168 -0
  366. package/src/protocol/schema.ts +76 -0
  367. package/src/protocol/values.ts +52 -0
  368. package/src/sql-split.ts +220 -0
  369. package/src/sql.ts +152 -0
  370. package/src/structure/StructureView.tsx +269 -0
  371. package/src/structure/structure-text.ts +4 -0
  372. package/src/table/CommandField.tsx +318 -0
  373. package/src/table/ImportDialog.tsx +168 -0
  374. package/src/table/TableStatusBar.tsx +91 -0
  375. package/src/table/TableToolbar.tsx +217 -0
  376. package/src/table/TableView.tsx +880 -0
  377. package/src/table/command-field.ts +285 -0
  378. package/src/table/grid-rows.ts +80 -0
  379. package/src/table/import-mapping.ts +37 -0
  380. package/src/table/layout-store.ts +62 -0
  381. package/src/table/paging.ts +33 -0
  382. package/src/table/pending.ts +143 -0
  383. package/src/table/references.ts +38 -0
  384. package/src/table/useLoaded.ts +58 -0
  385. package/src/table/useStoredLayout.ts +53 -0
  386. package/src/table/useTableTransfer.ts +187 -0
  387. package/src/testing/fake.ts +533 -0
  388. package/src/testing/index.ts +1 -0
  389. package/src/use-popup-press.ts +27 -0
  390. package/src/use-schema-change.ts +19 -0
  391. package/src/use-stable-callback.ts +12 -0
  392. package/src/value/ValuePanel.tsx +254 -0
  393. package/src/value/draft.ts +72 -0
  394. package/src/value/hex.ts +62 -0
  395. package/src/value/json.ts +74 -0
  396. package/src/value/views.ts +68 -0
  397. package/src/workbench/DatabaseWorkbench.tsx +348 -0
  398. package/src/workbench/DesignerTab.tsx +37 -0
  399. package/src/workbench/TableTab.tsx +27 -0
  400. package/src/workbench/tabs.ts +184 -0
@@ -0,0 +1,224 @@
1
+ import { useState, type Ref } from 'react';
2
+ import { CircleAlert, CircleCheck, Container, Database, Plus, Trash2, Unplug } from 'lucide-react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Banner, Button, EmptyState, Icon, Menu, messageOf, PromptDialog, Spinner } from '@adecore/ui';
5
+ import { DetailHeader, MasterDetail, MasterItem } from '@adecore/ui/settings';
6
+ import { useDatabaseClient } from '../client-context.ts';
7
+ import type { Connection } from '../client/types.ts';
8
+ import type { ConnectionConfig, DockerContainer, Engine, ServerInfo } from '../protocol/index.ts';
9
+ import { defaultConfig, isValidConfig, targetOf } from './connection-config.ts';
10
+ import { ConnectionForm } from './ConnectionForm.tsx';
11
+ import { composeNameOf, connectionFromContainer, isMysqlContainer } from './docker.ts';
12
+ import { useDockerContainers } from './use-docker-containers.ts';
13
+ import { ENGINE_ICONS } from './engine-icons.ts';
14
+ import { EngineIcon } from './EngineIcon.tsx';
15
+
16
+ const ENGINES: readonly Engine[] = ['sqlite', 'mysql'];
17
+
18
+ export interface ConnectionManagerProps {
19
+ value: readonly Connection[];
20
+ /* Saving is the app's: every add, edit and delete goes out whole through here. */
21
+ onValueChange(next: readonly Connection[]): void;
22
+ /* The id of the connection in the detail. Without it the manager keeps the selection itself; `null` is none. */
23
+ selected?: string | null;
24
+ onSelectedChange?(id: string | null): void;
25
+ /* Opens the app's file dialog for a database file or an SSH identity file; without it the path is typed. See `ConnectionForm`. */
26
+ onBrowse?(purpose: 'database' | 'identity'): Promise<string | null>;
27
+ className?: string;
28
+ ref?: Ref<HTMLDivElement>;
29
+ }
30
+
31
+ type TestOutcome = { readonly config: ConnectionConfig; readonly info: ServerInfo } | { readonly config: ConnectionConfig; readonly error: string };
32
+
33
+ /* The saved connections beside the form of the one picked, for a settings pane or a view of its own. */
34
+ export function ConnectionManager({ value, onValueChange, selected, onSelectedChange, onBrowse, className, ref }: ConnectionManagerProps) {
35
+ const { t } = useTranslation('database');
36
+ const [own, setOwn] = useState<string | null>(null);
37
+ const [deleting, setDeleting] = useState(false);
38
+ const client = useDatabaseClient();
39
+ const pickedId = selected === undefined ? own : selected;
40
+ const current = value.find((connection) => connection.id === pickedId) ?? value[0] ?? null;
41
+
42
+ const select = (id: string | null): void => {
43
+ if (selected === undefined) {
44
+ setOwn(id);
45
+ }
46
+ onSelectedChange?.(id);
47
+ };
48
+
49
+ const add = (engine: Engine): void => {
50
+ const connection: Connection = { id: crypto.randomUUID(), name: t('connections.newName'), config: defaultConfig(engine) };
51
+ onValueChange([...value, connection]);
52
+ select(connection.id);
53
+ };
54
+
55
+ const addFromContainer = (container: DockerContainer): void => {
56
+ const connection = connectionFromContainer(crypto.randomUUID(), container);
57
+ onValueChange([...value, connection]);
58
+ select(connection.id);
59
+ };
60
+
61
+ const change = (next: Connection): void => {
62
+ onValueChange(value.map((connection) => (connection.id === next.id ? next : connection)));
63
+ };
64
+
65
+ const remove = async (connection: Connection): Promise<void> => {
66
+ await client.disconnect(connection.id).catch(() => undefined);
67
+ const index = value.findIndex((entry) => entry.id === connection.id);
68
+ const rest = value.filter((entry) => entry.id !== connection.id);
69
+ onValueChange(rest);
70
+ select(rest[Math.min(index, rest.length - 1)]?.id ?? null);
71
+ };
72
+
73
+ const list = (
74
+ <>
75
+ <Menu.Root>
76
+ <Menu.Trigger render={<Button variant="secondary" size="sm" className="mb-2" />}>
77
+ <Icon icon={Plus} size={14} />
78
+ {t('connections.new')}
79
+ </Menu.Trigger>
80
+ <Menu.Popup>
81
+ {ENGINES.map((engine) => (
82
+ <Menu.Item key={engine} onClick={() => add(engine)}>
83
+ <Icon icon={ENGINE_ICONS[engine]} size={14} />
84
+ {t(`engine.${engine}`)}
85
+ </Menu.Item>
86
+ ))}
87
+ <Menu.Separator />
88
+ <Menu.SubmenuRoot>
89
+ <Menu.SubmenuTrigger>
90
+ <Icon icon={Container} size={14} />
91
+ {t('connections.fromDocker.label')}
92
+ </Menu.SubmenuTrigger>
93
+ <Menu.Popup>
94
+ <DockerMenuItems onPick={addFromContainer} />
95
+ </Menu.Popup>
96
+ </Menu.SubmenuRoot>
97
+ </Menu.Popup>
98
+ </Menu.Root>
99
+ {value.map((connection) => (
100
+ <MasterItem key={connection.id} selected={connection.id === current?.id} onSelect={() => select(connection.id)}>
101
+ <EngineIcon engine={connection.config.engine} size={16} className="shrink-0 text-text-muted" />
102
+ <span className="flex min-w-0 flex-col">
103
+ <span className="truncate">{connection.name || t('connections.untitled')}</span>
104
+ <span className="truncate text-xs text-text-faint">{targetOf(connection)}</span>
105
+ </span>
106
+ </MasterItem>
107
+ ))}
108
+ </>
109
+ );
110
+
111
+ const detail =
112
+ current === null ? (
113
+ <EmptyState icon={Database}>{t('connections.empty')}</EmptyState>
114
+ ) : (
115
+ <>
116
+ <DetailHeader
117
+ mark={<EngineIcon engine={current.config.engine} size={20} className="mt-0.5 shrink-0 text-text-muted" />}
118
+ title={current.name || t('connections.untitled')}
119
+ subtitle={targetOf(current)}
120
+ actions={
121
+ <Button variant="danger-outline" onClick={() => setDeleting(true)}>
122
+ <Icon icon={Trash2} size={14} />
123
+ {t('connections.delete.action')}
124
+ </Button>
125
+ }
126
+ />
127
+ <ConnectionForm value={current} onValueChange={change} onBrowse={onBrowse} />
128
+ <ConnectionTest key={current.id} connection={current} />
129
+ <PromptDialog
130
+ open={deleting}
131
+ danger
132
+ onOpenChange={setDeleting}
133
+ title={t('connections.delete.title')}
134
+ description={t('connections.delete.description', { name: current.name || t('connections.untitled') })}
135
+ confirmLabel={t('connections.delete.confirm')}
136
+ onConfirm={async () => {
137
+ await remove(current);
138
+ setDeleting(false);
139
+ }}
140
+ />
141
+ </>
142
+ );
143
+
144
+ return <MasterDetail ref={ref} className={className} list={list} listWidth={280} listLabel={t('connections.list')} detail={detail} />;
145
+ }
146
+
147
+ /* The containers Docker is running as menu items. The submenu mounts this when it opens, so Docker is asked only then. */
148
+ function DockerMenuItems({ onPick }: { onPick(container: DockerContainer): void }) {
149
+ const { t } = useTranslation('database');
150
+ const { state, reload } = useDockerContainers();
151
+ if (state.status === 'loading') {
152
+ return (
153
+ <Menu.Label className="flex items-center gap-2">
154
+ <Spinner size={12} />
155
+ {t('connections.fromDocker.loading')}
156
+ </Menu.Label>
157
+ );
158
+ }
159
+ if (state.status !== 'ready') {
160
+ return (
161
+ <>
162
+ <Menu.Label className="max-w-64 break-words">
163
+ {state.status === 'unsupported' ? `${t('connections.fromDocker.unsupported')} ${state.message}` : state.message}
164
+ </Menu.Label>
165
+ <Menu.Item closeOnClick={false} onClick={reload}>
166
+ {t('connections.fromDocker.retry')}
167
+ </Menu.Item>
168
+ </>
169
+ );
170
+ }
171
+ const containers = state.containers.filter(isMysqlContainer);
172
+ if (containers.length === 0) {
173
+ return <Menu.Label>{t('connections.fromDocker.empty')}</Menu.Label>;
174
+ }
175
+ return containers.map((container) => (
176
+ <Menu.Item key={container.id} onClick={() => onPick(container)}>
177
+ <span className="flex min-w-0 flex-col">
178
+ <span className="truncate">{composeNameOf(container) ?? container.name}</span>
179
+ <span className="truncate text-xs text-text-faint">{container.image}</span>
180
+ </span>
181
+ </Menu.Item>
182
+ ));
183
+ }
184
+
185
+ /* The button that asks the server who it is, and what it answered; an answer to an older config is not shown. */
186
+ function ConnectionTest({ connection }: { connection: Connection }) {
187
+ const { t } = useTranslation('database');
188
+ const client = useDatabaseClient();
189
+ const [busy, setBusy] = useState(false);
190
+ const [outcome, setOutcome] = useState<TestOutcome | null>(null);
191
+ const shown = outcome !== null && outcome.config === connection.config ? outcome : null;
192
+
193
+ const test = async (): Promise<void> => {
194
+ const config = connection.config;
195
+ setBusy(true);
196
+ try {
197
+ setOutcome({ config, info: await client.test(config) });
198
+ } catch (e) {
199
+ setOutcome({ config, error: messageOf(e, t('connections.test.failed')) });
200
+ } finally {
201
+ setBusy(false);
202
+ }
203
+ };
204
+
205
+ return (
206
+ <div className="flex flex-col gap-3">
207
+ <div>
208
+ <Button variant="secondary" disabled={busy || !isValidConfig(connection.config)} onClick={() => void test()}>
209
+ {busy ? <Spinner size={14} /> : <Icon icon={Unplug} size={14} />}
210
+ {t('connections.test.action')}
211
+ </Button>
212
+ </div>
213
+ {shown !== null && 'info' in shown && (
214
+ <Banner
215
+ icon={CircleCheck}
216
+ tone="neutral"
217
+ className="w-full"
218
+ message={t('connections.test.success', { server: `${t(`engine.flavor.${shown.info.flavor}`)} ${shown.info.version}` })}
219
+ />
220
+ )}
221
+ {shown !== null && 'error' in shown && <Banner icon={CircleAlert} tone="error" className="w-full" message={shown.error} />}
222
+ </div>
223
+ );
224
+ }
@@ -0,0 +1,18 @@
1
+ import type { Ref } from 'react';
2
+
3
+ import { Icon } from '@adecore/ui';
4
+ import type { Engine } from '../protocol/index.ts';
5
+ import { ENGINE_ICONS } from './engine-icons.ts';
6
+
7
+ export interface EngineIconProps {
8
+ engine: Engine;
9
+ /* 12, 14, 16 or 20. */
10
+ size?: number;
11
+ className?: string;
12
+ ref?: Ref<SVGSVGElement>;
13
+ }
14
+
15
+ /* A file on disk for SQLite, a server for MySQL and MariaDB. */
16
+ export function EngineIcon({ engine, size = 16, className, ref }: EngineIconProps) {
17
+ return <Icon ref={ref} icon={ENGINE_ICONS[engine]} size={size} className={className} />;
18
+ }
@@ -0,0 +1,143 @@
1
+ import type { ConnectionConfig, Engine, MysqlConnectionConfig, SqliteConnectionConfig } from '../protocol/index.ts';
2
+ import type { Connection } from '../client/types.ts';
3
+
4
+ export const DEFAULT_MYSQL_PORT = 3306;
5
+ export const DEFAULT_HOST = '127.0.0.1';
6
+
7
+ /* How a MySQL connection reaches its server. */
8
+ export type ConnectMode = 'tcp' | 'socket' | 'ssh' | 'docker';
9
+
10
+ export const CONNECT_MODES: readonly ConnectMode[] = ['tcp', 'socket', 'ssh', 'docker'];
11
+
12
+ /* What a field can be wrong about; the form maps each code onto a sentence. */
13
+ export type ConfigProblem =
14
+ | 'path-required'
15
+ | 'path-relative'
16
+ | 'host-required'
17
+ | 'port-range'
18
+ | 'socket-required'
19
+ | 'ssh-host-required'
20
+ | 'ssh-port-range'
21
+ | 'container-required'
22
+ | 'container-port-range';
23
+
24
+ export type ConfigProblems = Partial<Record<'path' | 'host' | 'port' | 'socket' | 'sshHost' | 'sshPort' | 'container' | 'containerPort', ConfigProblem>>;
25
+
26
+ /* A config of this engine with every field at what a new connection starts with. */
27
+ export const defaultConfig = (engine: Engine): ConnectionConfig =>
28
+ engine === 'sqlite'
29
+ ? { engine: 'sqlite', path: '', create: false, readOnly: false }
30
+ : { engine: 'mysql', host: DEFAULT_HOST, port: DEFAULT_MYSQL_PORT, user: '', password: '', tls: 'prefer', readOnly: false };
31
+
32
+ /* A POSIX path, a Windows drive path or a UNC path. A relative one would resolve against the helper's working directory. */
33
+ export const isAbsolutePath = (path: string): boolean => /^(?:\/|[A-Za-z]:[\\/]|\\\\)/.test(path);
34
+
35
+ const sqliteProblems = (config: SqliteConnectionConfig): ConfigProblems => {
36
+ const path = config.path.trim();
37
+ if (path === '') {
38
+ return { path: 'path-required' };
39
+ }
40
+ return isAbsolutePath(path) ? {} : { path: 'path-relative' };
41
+ };
42
+
43
+ const isPort = (port: number): boolean => Number.isInteger(port) && port >= 1 && port <= 65535;
44
+
45
+ /* The mode is in the config: a socket means the socket mode, even while its path is still empty, and a tunnel names its own. */
46
+ export const modeOf = (config: MysqlConnectionConfig): ConnectMode => {
47
+ if (config.tunnel !== undefined) {
48
+ return config.tunnel.kind;
49
+ }
50
+ return config.socket === undefined ? 'tcp' : 'socket';
51
+ };
52
+
53
+ const mysqlProblems = (config: MysqlConnectionConfig): ConfigProblems => {
54
+ const problems: { -readonly [K in keyof ConfigProblems]: ConfigProblems[K] } = {};
55
+ const tunnel = config.tunnel;
56
+ const mode = modeOf(config);
57
+ if (mode === 'socket' && (config.socket ?? '').trim() === '') {
58
+ problems.socket = 'socket-required';
59
+ }
60
+ if ((mode === 'tcp' || mode === 'ssh') && config.host.trim() === '') {
61
+ problems.host = 'host-required';
62
+ }
63
+ if ((mode === 'tcp' || mode === 'ssh') && config.port !== undefined && !isPort(config.port)) {
64
+ problems.port = 'port-range';
65
+ }
66
+ if (tunnel?.kind === 'ssh') {
67
+ if (tunnel.host.trim() === '') {
68
+ problems.sshHost = 'ssh-host-required';
69
+ }
70
+ if (tunnel.port !== undefined && !isPort(tunnel.port)) {
71
+ problems.sshPort = 'ssh-port-range';
72
+ }
73
+ }
74
+ if (tunnel?.kind === 'docker') {
75
+ if (tunnel.container.trim() === '') {
76
+ problems.container = 'container-required';
77
+ }
78
+ if (tunnel.port !== undefined && !isPort(tunnel.port)) {
79
+ problems.containerPort = 'container-port-range';
80
+ }
81
+ }
82
+ return problems;
83
+ };
84
+
85
+ export const configProblems = (config: ConnectionConfig): ConfigProblems => (config.engine === 'sqlite' ? sqliteProblems(config) : mysqlProblems(config));
86
+
87
+ export const isValidConfig = (config: ConnectionConfig): boolean => Object.keys(configProblems(config)).length === 0;
88
+
89
+ const lastSegment = (path: string): string => {
90
+ const segments = path.split(/[\\/]/).filter((segment) => segment !== '');
91
+ return segments[segments.length - 1] ?? path;
92
+ };
93
+
94
+ const targetWhere = (config: MysqlConnectionConfig): string => {
95
+ const tunnel = config.tunnel;
96
+ if (tunnel?.kind === 'docker') {
97
+ return tunnel.container === '' ? 'Docker' : `${tunnel.container} (Docker)`;
98
+ }
99
+ if (tunnel?.kind === 'ssh') {
100
+ return tunnel.host === '' ? config.host : `${config.host} via ${tunnel.host}`;
101
+ }
102
+ if (config.socket !== undefined) {
103
+ return lastSegment(config.socket);
104
+ }
105
+ return `${config.host}:${config.port ?? DEFAULT_MYSQL_PORT}`;
106
+ };
107
+
108
+ /* The short line under a connection's name: the file name of a database file, or `user@host:port`, `user@host via <ssh host>` and `user@container (Docker)`. */
109
+ export const targetOf = (connection: Connection): string => {
110
+ const config = connection.config;
111
+ if (config.engine === 'sqlite') {
112
+ return lastSegment(config.path);
113
+ }
114
+ const where = targetWhere(config);
115
+ return config.user === '' || where === '' ? where : `${config.user}@${where}`;
116
+ };
117
+
118
+ /* Moves to another way of reaching the server. What the person typed for the user, the password and the database stays. */
119
+ export const withMode = (config: MysqlConnectionConfig, mode: ConnectMode): MysqlConnectionConfig => {
120
+ if (modeOf(config) === mode) {
121
+ return config;
122
+ }
123
+ const base: MysqlConnectionConfig = {
124
+ ...config,
125
+ host: config.host.trim() === '' ? DEFAULT_HOST : config.host,
126
+ socket: undefined,
127
+ tunnel: undefined
128
+ };
129
+ switch (mode) {
130
+ case 'tcp':
131
+ return base;
132
+ case 'socket':
133
+ return { ...base, socket: '' };
134
+ case 'ssh':
135
+ return { ...base, tunnel: { kind: 'ssh', host: '' } };
136
+ case 'docker':
137
+ return { ...base, tunnel: { kind: 'docker', container: '', port: DEFAULT_MYSQL_PORT } };
138
+ }
139
+ };
140
+
141
+ /* Moves to another engine, which keeps who the connection is and drops how it connects. */
142
+ export const withEngine = (connection: Connection, engine: Engine): Connection =>
143
+ connection.config.engine === engine ? connection : { id: connection.id, name: connection.name, config: defaultConfig(engine) };
@@ -0,0 +1,50 @@
1
+ import type { Connection } from '../client/types.ts';
2
+ import type { DockerContainer, MysqlConnectionConfig } from '../protocol/index.ts';
3
+ import { DEFAULT_MYSQL_PORT, defaultConfig, withMode } from './connection-config.ts';
4
+
5
+ /* A container a MySQL connection can point at: what `discover` found, minus a file engine, which no container serves. */
6
+ export const isMysqlContainer = (container: DockerContainer): boolean => container.engine !== 'sqlite';
7
+
8
+ /* The Compose `project/service` of a container, when it has both. */
9
+ export const composeNameOf = (container: DockerContainer): string | null =>
10
+ container.project !== null && container.service !== null ? `${container.project}/${container.service}` : null;
11
+
12
+ /* What names a container to a person: its Compose name, else its own. */
13
+ export const containerTitle = (container: DockerContainer): string => composeNameOf(container) ?? container.name;
14
+
15
+ /* The port to connect to inside the container: MySQL's own when the container lists it or lists nothing, else the first it lists. */
16
+ export const containerPortOf = (container: DockerContainer): number => {
17
+ const ports = container.ports.map((entry) => entry.container);
18
+ return ports.length === 0 || ports.includes(DEFAULT_MYSQL_PORT) ? DEFAULT_MYSQL_PORT : ports[0]!;
19
+ };
20
+
21
+ /* The host port a container publishes a port on, or `null` when it does not. */
22
+ export const publishedPortOf = (container: DockerContainer, port: number): number | null =>
23
+ container.ports.find((entry) => entry.container === port)?.host ?? null;
24
+
25
+ const isEmpty = (text: string | undefined): boolean => text === undefined || text === '';
26
+
27
+ /* Points a config at a container. The user, the password and the database come from the container's environment only where the config has none. */
28
+ export const withContainer = (config: MysqlConnectionConfig, container: DockerContainer): MysqlConnectionConfig => {
29
+ const docker = withMode(config, 'docker');
30
+ const { user, password, database } = container.suggested;
31
+ return {
32
+ ...docker,
33
+ user: isEmpty(docker.user) && user !== undefined ? user : docker.user,
34
+ password: isEmpty(docker.password) && password !== undefined ? password : docker.password,
35
+ database: isEmpty(docker.database) && database !== undefined ? database : docker.database,
36
+ tunnel: {
37
+ kind: 'docker',
38
+ container: container.name,
39
+ port: containerPortOf(container),
40
+ ...(docker.tunnel?.kind === 'docker' && docker.tunnel.context !== undefined ? { context: docker.tunnel.context } : {})
41
+ }
42
+ };
43
+ };
44
+
45
+ /* A new MySQL connection to a container, named after its Compose `project/service` or its own name. */
46
+ export const connectionFromContainer = (id: string, container: DockerContainer): Connection => ({
47
+ id,
48
+ name: containerTitle(container),
49
+ config: withContainer(defaultConfig('mysql') as MysqlConnectionConfig, container)
50
+ });
@@ -0,0 +1,5 @@
1
+ import { HardDrive, Server, type LucideIcon } from 'lucide-react';
2
+ import type { Engine } from '../protocol/index.ts';
3
+
4
+ /* A file on disk for SQLite, a server for MySQL and MariaDB. */
5
+ export const ENGINE_ICONS: Record<Engine, LucideIcon> = { sqlite: HardDrive, mysql: Server };
@@ -0,0 +1,41 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { messageOf } from '@adecore/ui';
3
+ import { useDatabaseClient } from '../client-context.ts';
4
+ import { DatabaseRequestError } from '../client/types.ts';
5
+ import type { DockerContainer } from '../protocol/index.ts';
6
+
7
+ /* `unsupported` is a machine without Docker, or one the app cannot ask. */
8
+ export type DockerState =
9
+ | { readonly status: 'loading' }
10
+ | { readonly status: 'ready'; readonly containers: readonly DockerContainer[] }
11
+ | { readonly status: 'unsupported'; readonly message: string }
12
+ | { readonly status: 'error'; readonly message: string };
13
+
14
+ /* The running containers that look like a database, asked for when the component mounts and again on `reload`. */
15
+ export function useDockerContainers(): { readonly state: DockerState; reload(): void } {
16
+ const client = useDatabaseClient();
17
+ const [state, setState] = useState<DockerState>({ status: 'loading' });
18
+ const [attempt, setAttempt] = useState(0);
19
+
20
+ useEffect(() => {
21
+ const controller = new AbortController();
22
+ client.discover('docker', { signal: controller.signal }).then(
23
+ (containers) => setState({ status: 'ready', containers }),
24
+ (e: unknown) => {
25
+ if (controller.signal.aborted) {
26
+ return;
27
+ }
28
+ const message = messageOf(e);
29
+ setState({ status: e instanceof DatabaseRequestError && e.code === 'unsupported' ? 'unsupported' : 'error', message });
30
+ }
31
+ );
32
+ return () => controller.abort();
33
+ }, [client, attempt]);
34
+
35
+ const reload = (): void => {
36
+ setState({ status: 'loading' });
37
+ setAttempt((current) => current + 1);
38
+ };
39
+
40
+ return { state, reload };
41
+ }
@@ -0,0 +1,40 @@
1
+ import type { Ref } from 'react';
2
+ import { TriangleAlert } from 'lucide-react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { PromptDialog } from '@adecore/ui';
5
+ import type { DestructiveStatement } from './destructive.ts';
6
+
7
+ export interface DestructiveDialogProps {
8
+ /* The statements to warn about; the dialog is open while there are any. */
9
+ statements: readonly DestructiveStatement[];
10
+ onConfirm(): void;
11
+ onCancel(): void;
12
+ ref?: Ref<HTMLDivElement>;
13
+ }
14
+
15
+ /* Lists the statements that cannot be taken back and asks before they run. */
16
+ export function DestructiveDialog({ statements, onConfirm, onCancel, ref }: DestructiveDialogProps) {
17
+ const { t } = useTranslation('database');
18
+ return (
19
+ <PromptDialog
20
+ ref={ref}
21
+ open={statements.length > 0}
22
+ danger
23
+ title={t('console.destructive.title')}
24
+ titleIcon={TriangleAlert}
25
+ description={t('console.destructive.description', { count: statements.length })}
26
+ confirmLabel={t('console.destructive.confirm')}
27
+ onConfirm={onConfirm}
28
+ onOpenChange={onCancel}
29
+ >
30
+ <ul className="mt-3 flex max-h-56 flex-col gap-2 overflow-auto">
31
+ {statements.map((statement, index) => (
32
+ <li key={`${index}:${statement.sql}`} className="flex min-w-0 flex-col gap-0.5 rounded-md bg-surface-sunken px-2.5 py-1.5">
33
+ <span className="text-xs text-status-error">{t(`console.destructive.${statement.kind}`)}</span>
34
+ <span className="truncate font-mono text-xs text-text">{statement.sql.replace(/\s+/g, ' ')}</span>
35
+ </li>
36
+ ))}
37
+ </ul>
38
+ </PromptDialog>
39
+ );
40
+ }
@@ -0,0 +1,67 @@
1
+ import { useState, type Ref } from 'react';
2
+ import clsx from 'clsx';
3
+ import { Check, CircleAlert, Search, Trash2 } from 'lucide-react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Icon, IconButton, Input, isApplePlatform, isModHeld } from '@adecore/ui';
6
+ import { formatMoment, formatNumber } from '@adecore/ui/format';
7
+ import { searchHistory, type HistoryEntry } from './history.ts';
8
+
9
+ export interface HistoryPanelProps {
10
+ entries: readonly HistoryEntry[];
11
+ /* Clicking puts the SQL in the editor, and Mod+click or a double click runs it as well. */
12
+ onPick(entry: HistoryEntry, run: boolean): void;
13
+ onClear(): void;
14
+ className?: string;
15
+ ref?: Ref<HTMLElement>;
16
+ }
17
+
18
+ /* The runs of this connection, newest first, with a search over their SQL. */
19
+ export function HistoryPanel({ entries, onPick, onClear, className, ref }: HistoryPanelProps) {
20
+ const { t } = useTranslation('database');
21
+ const [query, setQuery] = useState('');
22
+ const shown = searchHistory(entries, query);
23
+
24
+ return (
25
+ <aside ref={ref} aria-label={t('console.history.title')} className={clsx('flex w-72 shrink-0 flex-col border-l border-border', className)}>
26
+ <div className="flex shrink-0 items-center gap-2 p-2">
27
+ <Input
28
+ size="sm"
29
+ type="search"
30
+ icon={Search}
31
+ className="min-w-0 flex-1"
32
+ aria-label={t('console.history.search')}
33
+ placeholder={t('console.history.search')}
34
+ value={query}
35
+ onChange={(event) => setQuery(event.target.value)}
36
+ />
37
+ <IconButton icon={Trash2} size="sm" label={t('console.history.clear')} disabled={entries.length === 0} onClick={onClear} />
38
+ </div>
39
+ <div className="relative min-h-24 flex-1 border-t border-border">
40
+ {shown.length === 0 ? (
41
+ <p className="p-3 text-xs text-text-muted">{entries.length === 0 ? t('console.history.empty') : t('console.history.noMatches')}</p>
42
+ ) : (
43
+ <ul className="absolute inset-0 overflow-auto">
44
+ {shown.map((entry) => (
45
+ <li key={`${entry.at}:${entry.sql}`} className="border-b border-border-soft">
46
+ <button
47
+ type="button"
48
+ className="flex w-full min-w-0 flex-col gap-0.5 px-3 py-2 text-left hover:bg-surface-hover"
49
+ onClick={(event) => onPick(entry, isModHeld(event, isApplePlatform()))}
50
+ onDoubleClick={() => onPick(entry, true)}
51
+ >
52
+ <span className="truncate font-mono text-xs text-text">{entry.sql.replace(/\s+/g, ' ')}</span>
53
+ <span className="flex items-center gap-1.5 text-xs text-text-muted tabular-nums">
54
+ <Icon icon={entry.ok ? Check : CircleAlert} size={12} className={entry.ok ? 'text-positive' : 'text-status-error'} />
55
+ {formatMoment(entry.at)}
56
+ {entry.rows !== null && t('console.history.rows', { count: entry.rows, formatted: formatNumber(entry.rows) })}
57
+ </span>
58
+ </button>
59
+ </li>
60
+ ))}
61
+ </ul>
62
+ )}
63
+ </div>
64
+ <p className="shrink-0 border-t border-border px-3 py-2 text-xs text-text-faint">{t('console.history.hint')}</p>
65
+ </aside>
66
+ );
67
+ }