@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,10 @@
1
+ import { type Ref } from 'react';
2
+ import type { Connection } from '../client/types.ts';
3
+ export interface ConnectionFormProps {
4
+ value: Connection;
5
+ onValueChange(next: Connection): void;
6
+ onBrowse?(purpose: 'database' | 'identity'): Promise<string | null>;
7
+ className?: string;
8
+ ref?: Ref<HTMLDivElement>;
9
+ }
10
+ export declare function ConnectionForm({ value, onValueChange, onBrowse, className, ref }: ConnectionFormProps): import("react").JSX.Element;
@@ -0,0 +1,145 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import clsx from 'clsx';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Container, Network, Plug, RefreshCw, Terminal } from 'lucide-react';
6
+ import { Button, Field, IconButton, Input, Segmented, Select, Switch, useAsyncAction } from '@adecore/ui';
7
+ import { CONNECT_MODES, DEFAULT_HOST, DEFAULT_MYSQL_PORT, configProblems, modeOf, withEngine, withMode } from './connection-config.js';
8
+ import { composeNameOf, containerPortOf, isMysqlContainer, publishedPortOf, withContainer } from './docker.js';
9
+ import { ENGINE_ICONS } from './engine-icons.js';
10
+ import { useDockerContainers } from './use-docker-containers.js';
11
+ const TLS_MODES = ['disable', 'prefer', 'require', 'verify'];
12
+ const MODE_ICONS = { tcp: Network, socket: Plug, ssh: Terminal, docker: Container };
13
+ /* A text field that is empty means "not set" for the optional fields of a config. */
14
+ const optionalText = (text) => (text === '' ? undefined : text);
15
+ /* One form for one saved connection. Every edit goes out whole through `onValueChange`. */
16
+ export function ConnectionForm({ value, onValueChange, onBrowse, className, ref }) {
17
+ const { t } = useTranslation('database');
18
+ const [touched, setTouched] = useState(new Set());
19
+ const config = value.config;
20
+ const options = [
21
+ { id: 'sqlite', label: t('engine.sqlite'), icon: ENGINE_ICONS.sqlite },
22
+ { id: 'mysql', label: t('engine.mysql'), icon: ENGINE_ICONS.mysql }
23
+ ];
24
+ const touch = (field) => {
25
+ setTouched((current) => new Set(current).add(field));
26
+ };
27
+ return (_jsxs("div", { ref: ref, className: clsx('flex flex-col gap-4', className), children: [_jsx(Field, { label: t('connections.form.name'), orientation: "horizontal", children: _jsx(Input, { value: value.name, placeholder: t('connections.untitled'), onChange: (e) => onValueChange({ ...value, name: e.target.value }) }) }), _jsx(Field, { label: t('connections.form.engine'), orientation: "horizontal", group: true, children: _jsx(Segmented, { value: config.engine, label: t('connections.form.engine'), options: options, onValueChange: (engine) => {
28
+ onValueChange(withEngine(value, engine));
29
+ setTouched(new Set());
30
+ } }) }), config.engine === 'sqlite' ? (_jsx(SqliteFields, { config: config, onConfigChange: (next) => onValueChange({ ...value, config: next }), touched: touched, touch: touch, onBrowse: onBrowse })) : (_jsx(MysqlFields, { config: config, onConfigChange: (next) => onValueChange({ ...value, config: next }), touched: touched, touch: touch, onBrowse: onBrowse })), _jsx(Field, { label: t('connections.form.readOnly'), hint: t('connections.form.readOnlyHint'), orientation: "horizontal", group: true, children: _jsx(Switch, { checked: config.readOnly === true, label: t('connections.form.readOnly'), onCheckedChange: (readOnly) => onValueChange({ ...value, config: { ...config, readOnly } }) }) })] }));
31
+ }
32
+ /* The sentence for a problem, once the field has something to answer for. */
33
+ function useProblem(problem, field, touched, filled) {
34
+ const { t } = useTranslation('database');
35
+ if (problem === undefined || !(touched.has(field) || filled)) {
36
+ return undefined;
37
+ }
38
+ return t(`connections.form.problems.${problem}`);
39
+ }
40
+ function SqliteFields({ config, onConfigChange, touched, touch, onBrowse }) {
41
+ const { t } = useTranslation('database');
42
+ const browsing = useAsyncAction();
43
+ const problem = configProblems(config).path;
44
+ const error = useProblem(problem, 'path', touched, config.path !== '') ?? browsing.failure ?? undefined;
45
+ const browse = async () => {
46
+ await browsing.run(async () => {
47
+ const picked = await onBrowse?.('database');
48
+ if (picked !== null && picked !== undefined) {
49
+ onConfigChange({ ...config, path: picked });
50
+ }
51
+ });
52
+ touch('path');
53
+ };
54
+ return (_jsxs(_Fragment, { children: [_jsx(Field, { label: t('connections.form.path'), hint: t('connections.form.pathHint'), error: error, orientation: "horizontal", group: onBrowse !== undefined, children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { mono: true, value: config.path, placeholder: "/path/to/database.sqlite", spellCheck: false, "aria-label": onBrowse === undefined ? undefined : t('connections.form.path'), onBlur: () => touch('path'), onChange: (e) => onConfigChange({ ...config, path: e.target.value }) }), onBrowse !== undefined && (_jsx(Button, { variant: "secondary", disabled: browsing.busy, onClick: () => void browse(), children: t('connections.form.browse') }))] }) }), _jsx(Field, { label: t('connections.form.create'), hint: t('connections.form.createHint'), orientation: "horizontal", group: true, children: _jsx(Switch, { checked: config.create === true, label: t('connections.form.create'), onCheckedChange: (create) => onConfigChange({ ...config, create }) }) })] }));
55
+ }
56
+ function PortField({ label, hint, error, value, placeholder, onBlur, onValueChange }) {
57
+ return (_jsx(Field, { label: label, hint: hint, error: error, orientation: "horizontal", children: _jsx(Input, { type: "number", min: 1, max: 65535, className: "max-w-32", value: value === undefined ? '' : String(value), placeholder: String(placeholder), onBlur: onBlur, onChange: (e) => onValueChange(e.target.value === '' ? undefined : Number(e.target.value)) }) }));
58
+ }
59
+ function MysqlFields({ config, onConfigChange, touched, touch, onBrowse }) {
60
+ const { t } = useTranslation('database');
61
+ const mode = modeOf(config);
62
+ const tunnel = config.tunnel;
63
+ const modes = CONNECT_MODES.map((id) => ({ id, label: t(`connections.form.mode.${id}`), icon: MODE_ICONS[id] }));
64
+ const tlsItems = TLS_MODES.map((tls) => ({
65
+ value: tls,
66
+ label: t(`connections.form.tls.${tls}`),
67
+ description: t(`connections.form.tls.${tls}Hint`)
68
+ }));
69
+ return (_jsxs(_Fragment, { children: [_jsx(Field, { label: t('connections.form.mode.label'), orientation: "horizontal", group: true, children: _jsx(Segmented, { value: mode, label: t('connections.form.mode.label'), options: modes, onValueChange: (next) => onConfigChange(withMode(config, next)) }) }), mode === 'tcp' && _jsx(ServerFields, { config: config, onConfigChange: onConfigChange, touched: touched, touch: touch }), mode === 'socket' && _jsx(SocketField, { config: config, onConfigChange: onConfigChange, touched: touched, touch: touch }), tunnel?.kind === 'ssh' && (_jsx(SshFields, { config: config, tunnel: tunnel, onConfigChange: onConfigChange, touched: touched, touch: touch, onBrowse: onBrowse })), tunnel?.kind === 'docker' && _jsx(DockerFields, { config: config, tunnel: tunnel, onConfigChange: onConfigChange, touched: touched, touch: touch }), _jsx(Field, { label: t('connections.form.user'), orientation: "horizontal", children: _jsx(Input, { value: config.user, autoComplete: "off", spellCheck: false, onChange: (e) => onConfigChange({ ...config, user: e.target.value }) }) }), _jsx(Field, { label: t('connections.form.password'), orientation: "horizontal", children: _jsx(Input, { type: "password", value: config.password ?? '', autoComplete: "off", onChange: (e) => onConfigChange({ ...config, password: optionalText(e.target.value) }) }) }), _jsx(Field, { label: t('connections.form.database'), hint: t('connections.form.databaseHint'), orientation: "horizontal", children: _jsx(Input, { value: config.database ?? '', spellCheck: false, onChange: (e) => onConfigChange({ ...config, database: optionalText(e.target.value) }) }) }), _jsx(Field, { label: t('connections.form.tls.label'), orientation: "horizontal", group: true, children: _jsx(Select, { value: config.tls ?? 'prefer', label: t('connections.form.tls.label'), items: tlsItems, onValueChange: (tls) => onConfigChange({ ...config, tls }) }) })] }));
70
+ }
71
+ /* The host and the port of a server reached directly, or, through SSH, as the SSH host sees it. */
72
+ function ServerFields({ config, onConfigChange, touched, touch }) {
73
+ const { t } = useTranslation('database');
74
+ const problems = configProblems(config);
75
+ const viaSsh = config.tunnel?.kind === 'ssh';
76
+ const hostError = useProblem(problems.host, 'host', touched, config.host !== '');
77
+ const portError = useProblem(problems.port, 'port', touched, config.port !== undefined);
78
+ return (_jsxs(_Fragment, { children: [_jsx(Field, { label: t(viaSsh ? 'connections.form.ssh.serverHost' : 'connections.form.host'), hint: viaSsh ? t('connections.form.ssh.serverHint') : undefined, error: hostError, orientation: "horizontal", children: _jsx(Input, { value: config.host, placeholder: DEFAULT_HOST, spellCheck: false, onBlur: () => touch('host'), onChange: (e) => onConfigChange({ ...config, host: e.target.value }) }) }), _jsx(PortField, { label: t(viaSsh ? 'connections.form.ssh.serverPort' : 'connections.form.port'), error: portError, value: config.port, placeholder: DEFAULT_MYSQL_PORT, onBlur: () => touch('port'), onValueChange: (port) => onConfigChange({ ...config, port }) })] }));
79
+ }
80
+ function SocketField({ config, onConfigChange, touched, touch }) {
81
+ const { t } = useTranslation('database');
82
+ const error = useProblem(configProblems(config).socket, 'socket', touched, (config.socket ?? '') !== '');
83
+ return (_jsx(Field, { label: t('connections.form.socket'), hint: t('connections.form.socketHint'), error: error, orientation: "horizontal", children: _jsx(Input, { mono: true, value: config.socket ?? '', placeholder: "/tmp/mysql.sock", spellCheck: false, onBlur: () => touch('socket'), onChange: (e) => onConfigChange({ ...config, socket: e.target.value }) }) }));
84
+ }
85
+ function SshFields({ config, tunnel, onConfigChange, touched, touch, onBrowse }) {
86
+ const { t } = useTranslation('database');
87
+ const browsing = useAsyncAction();
88
+ const problems = configProblems(config);
89
+ const hostError = useProblem(problems.sshHost, 'sshHost', touched, tunnel.host !== '');
90
+ const portError = useProblem(problems.sshPort, 'sshPort', touched, tunnel.port !== undefined);
91
+ const update = (patch) => onConfigChange({ ...config, tunnel: { ...tunnel, ...patch } });
92
+ const browse = async () => {
93
+ await browsing.run(async () => {
94
+ const picked = await onBrowse?.('identity');
95
+ if (picked !== null && picked !== undefined) {
96
+ update({ identityFile: picked });
97
+ }
98
+ });
99
+ };
100
+ return (_jsxs(_Fragment, { children: [_jsx(Field, { label: t('connections.form.ssh.host'), hint: t('connections.form.ssh.hostHint'), error: hostError, orientation: "horizontal", children: _jsx(Input, { value: tunnel.host, placeholder: "bastion.example.com", spellCheck: false, onBlur: () => touch('sshHost'), onChange: (e) => update({ host: e.target.value }) }) }), _jsx(PortField, { label: t('connections.form.ssh.port'), error: portError, value: tunnel.port, placeholder: 22, onBlur: () => touch('sshPort'), onValueChange: (port) => update({ port }) }), _jsx(Field, { label: t('connections.form.ssh.user'), orientation: "horizontal", children: _jsx(Input, { value: tunnel.user ?? '', autoComplete: "off", spellCheck: false, onChange: (e) => update({ user: optionalText(e.target.value) }) }) }), _jsx(Field, { label: t('connections.form.ssh.identityFile'), hint: t('connections.form.ssh.keyHint'), error: browsing.failure ?? undefined, orientation: "horizontal", group: onBrowse !== undefined, children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Input, { mono: true, value: tunnel.identityFile ?? '', placeholder: "~/.ssh/id_ed25519", spellCheck: false, "aria-label": onBrowse === undefined ? undefined : t('connections.form.ssh.identityFile'), onChange: (e) => update({ identityFile: optionalText(e.target.value) }) }), onBrowse !== undefined && (_jsx(Button, { variant: "secondary", disabled: browsing.busy, onClick: () => void browse(), children: t('connections.form.browse') }))] }) }), _jsx(ServerFields, { config: config, onConfigChange: onConfigChange, touched: touched, touch: touch })] }));
101
+ }
102
+ /* What a container offers a person in a list: its image, its Compose name and the port it publishes. */
103
+ function containerDescription(container, published) {
104
+ return [container.image, composeNameOf(container), published].filter((part) => part !== null).join(' \u00b7 ');
105
+ }
106
+ function DockerFields({ config, tunnel, onConfigChange, touched, touch }) {
107
+ const { t } = useTranslation('database');
108
+ const docker = useDockerContainers();
109
+ const problems = configProblems(config);
110
+ const containers = docker.state.status === 'ready' ? docker.state.containers.filter(isMysqlContainer) : [];
111
+ const picked = containers.find((container) => container.name === tunnel.container);
112
+ const containerError = useProblem(problems.container, 'container', touched, tunnel.container !== '');
113
+ const portError = useProblem(problems.containerPort, 'containerPort', touched, tunnel.port !== undefined);
114
+ const publishedText = (container, port) => {
115
+ const host = publishedPortOf(container, port);
116
+ return host === null ? t('connections.form.docker.notPublished') : t('connections.form.docker.published', { port: host });
117
+ };
118
+ const items = containers.map((container) => ({
119
+ value: container.name,
120
+ label: container.name,
121
+ description: containerDescription(container, publishedText(container, containerPortOf(container)))
122
+ }));
123
+ if (tunnel.container !== '' && picked === undefined) {
124
+ items.unshift({ value: tunnel.container, label: tunnel.container, description: t('connections.form.docker.notRunning') });
125
+ }
126
+ const status = (() => {
127
+ switch (docker.state.status) {
128
+ case 'loading':
129
+ return { hint: t('connections.form.docker.loading') };
130
+ case 'unsupported':
131
+ return { hint: `${t('connections.form.docker.unsupported')} ${docker.state.message}` };
132
+ case 'error':
133
+ return { error: docker.state.message };
134
+ case 'ready':
135
+ return containers.length === 0 ? { hint: t('connections.form.docker.empty') } : {};
136
+ }
137
+ })();
138
+ const pick = (name) => {
139
+ const container = containers.find((candidate) => candidate.name === name);
140
+ if (container !== undefined) {
141
+ onConfigChange(withContainer(config, container));
142
+ }
143
+ };
144
+ return (_jsxs(_Fragment, { children: [_jsx(Field, { label: t('connections.form.docker.container'), hint: status.hint, error: status.error ?? containerError, orientation: "horizontal", group: true, children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Select, { value: tunnel.container === '' ? null : tunnel.container, label: t('connections.form.docker.container'), placeholder: t('connections.form.docker.pick'), items: items, truncateValue: true, disabled: items.length === 0, onValueChange: pick }), _jsx(IconButton, { icon: RefreshCw, label: t('connections.form.docker.refresh'), busy: docker.state.status === 'loading', onClick: docker.reload, onBlur: () => touch('container') })] }) }), _jsx(PortField, { label: t('connections.form.docker.port'), hint: picked === undefined ? undefined : publishedText(picked, tunnel.port ?? DEFAULT_MYSQL_PORT), error: portError, value: tunnel.port, placeholder: DEFAULT_MYSQL_PORT, onBlur: () => touch('containerPort'), onValueChange: (port) => onConfigChange({ ...config, tunnel: { ...tunnel, port } }) })] }));
145
+ }
@@ -0,0 +1,12 @@
1
+ import { type Ref } from 'react';
2
+ import type { Connection } from '../client/types.ts';
3
+ export interface ConnectionManagerProps {
4
+ value: readonly Connection[];
5
+ onValueChange(next: readonly Connection[]): void;
6
+ selected?: string | null;
7
+ onSelectedChange?(id: string | null): void;
8
+ onBrowse?(purpose: 'database' | 'identity'): Promise<string | null>;
9
+ className?: string;
10
+ ref?: Ref<HTMLDivElement>;
11
+ }
12
+ export declare function ConnectionManager({ value, onValueChange, selected, onSelectedChange, onBrowse, className, ref }: ConnectionManagerProps): import("react").JSX.Element;
@@ -0,0 +1,93 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { CircleAlert, CircleCheck, Container, Database, Plus, Trash2, Unplug } from 'lucide-react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { Banner, Button, EmptyState, Icon, Menu, messageOf, PromptDialog, Spinner } from '@adecore/ui';
6
+ import { DetailHeader, MasterDetail, MasterItem } from '@adecore/ui/settings';
7
+ import { useDatabaseClient } from '../client-context.js';
8
+ import { defaultConfig, isValidConfig, targetOf } from './connection-config.js';
9
+ import { ConnectionForm } from './ConnectionForm.js';
10
+ import { composeNameOf, connectionFromContainer, isMysqlContainer } from './docker.js';
11
+ import { useDockerContainers } from './use-docker-containers.js';
12
+ import { ENGINE_ICONS } from './engine-icons.js';
13
+ import { EngineIcon } from './EngineIcon.js';
14
+ const ENGINES = ['sqlite', 'mysql'];
15
+ /* The saved connections beside the form of the one picked, for a settings pane or a view of its own. */
16
+ export function ConnectionManager({ value, onValueChange, selected, onSelectedChange, onBrowse, className, ref }) {
17
+ const { t } = useTranslation('database');
18
+ const [own, setOwn] = useState(null);
19
+ const [deleting, setDeleting] = useState(false);
20
+ const client = useDatabaseClient();
21
+ const pickedId = selected === undefined ? own : selected;
22
+ const current = value.find((connection) => connection.id === pickedId) ?? value[0] ?? null;
23
+ const select = (id) => {
24
+ if (selected === undefined) {
25
+ setOwn(id);
26
+ }
27
+ onSelectedChange?.(id);
28
+ };
29
+ const add = (engine) => {
30
+ const connection = { id: crypto.randomUUID(), name: t('connections.newName'), config: defaultConfig(engine) };
31
+ onValueChange([...value, connection]);
32
+ select(connection.id);
33
+ };
34
+ const addFromContainer = (container) => {
35
+ const connection = connectionFromContainer(crypto.randomUUID(), container);
36
+ onValueChange([...value, connection]);
37
+ select(connection.id);
38
+ };
39
+ const change = (next) => {
40
+ onValueChange(value.map((connection) => (connection.id === next.id ? next : connection)));
41
+ };
42
+ const remove = async (connection) => {
43
+ await client.disconnect(connection.id).catch(() => undefined);
44
+ const index = value.findIndex((entry) => entry.id === connection.id);
45
+ const rest = value.filter((entry) => entry.id !== connection.id);
46
+ onValueChange(rest);
47
+ select(rest[Math.min(index, rest.length - 1)]?.id ?? null);
48
+ };
49
+ const list = (_jsxs(_Fragment, { children: [_jsxs(Menu.Root, { children: [_jsxs(Menu.Trigger, { render: _jsx(Button, { variant: "secondary", size: "sm", className: "mb-2" }), children: [_jsx(Icon, { icon: Plus, size: 14 }), t('connections.new')] }), _jsxs(Menu.Popup, { children: [ENGINES.map((engine) => (_jsxs(Menu.Item, { onClick: () => add(engine), children: [_jsx(Icon, { icon: ENGINE_ICONS[engine], size: 14 }), t(`engine.${engine}`)] }, engine))), _jsx(Menu.Separator, {}), _jsxs(Menu.SubmenuRoot, { children: [_jsxs(Menu.SubmenuTrigger, { children: [_jsx(Icon, { icon: Container, size: 14 }), t('connections.fromDocker.label')] }), _jsx(Menu.Popup, { children: _jsx(DockerMenuItems, { onPick: addFromContainer }) })] })] })] }), value.map((connection) => (_jsxs(MasterItem, { selected: connection.id === current?.id, onSelect: () => select(connection.id), children: [_jsx(EngineIcon, { engine: connection.config.engine, size: 16, className: "shrink-0 text-text-muted" }), _jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate", children: connection.name || t('connections.untitled') }), _jsx("span", { className: "truncate text-xs text-text-faint", children: targetOf(connection) })] })] }, connection.id)))] }));
50
+ const detail = current === null ? (_jsx(EmptyState, { icon: Database, children: t('connections.empty') })) : (_jsxs(_Fragment, { children: [_jsx(DetailHeader, { mark: _jsx(EngineIcon, { engine: current.config.engine, size: 20, className: "mt-0.5 shrink-0 text-text-muted" }), title: current.name || t('connections.untitled'), subtitle: targetOf(current), actions: _jsxs(Button, { variant: "danger-outline", onClick: () => setDeleting(true), children: [_jsx(Icon, { icon: Trash2, size: 14 }), t('connections.delete.action')] }) }), _jsx(ConnectionForm, { value: current, onValueChange: change, onBrowse: onBrowse }), _jsx(ConnectionTest, { connection: current }, current.id), _jsx(PromptDialog, { open: deleting, danger: true, onOpenChange: setDeleting, title: t('connections.delete.title'), description: t('connections.delete.description', { name: current.name || t('connections.untitled') }), confirmLabel: t('connections.delete.confirm'), onConfirm: async () => {
51
+ await remove(current);
52
+ setDeleting(false);
53
+ } })] }));
54
+ return _jsx(MasterDetail, { ref: ref, className: className, list: list, listWidth: 280, listLabel: t('connections.list'), detail: detail });
55
+ }
56
+ /* The containers Docker is running as menu items. The submenu mounts this when it opens, so Docker is asked only then. */
57
+ function DockerMenuItems({ onPick }) {
58
+ const { t } = useTranslation('database');
59
+ const { state, reload } = useDockerContainers();
60
+ if (state.status === 'loading') {
61
+ return (_jsxs(Menu.Label, { className: "flex items-center gap-2", children: [_jsx(Spinner, { size: 12 }), t('connections.fromDocker.loading')] }));
62
+ }
63
+ if (state.status !== 'ready') {
64
+ return (_jsxs(_Fragment, { children: [_jsx(Menu.Label, { className: "max-w-64 break-words", children: state.status === 'unsupported' ? `${t('connections.fromDocker.unsupported')} ${state.message}` : state.message }), _jsx(Menu.Item, { closeOnClick: false, onClick: reload, children: t('connections.fromDocker.retry') })] }));
65
+ }
66
+ const containers = state.containers.filter(isMysqlContainer);
67
+ if (containers.length === 0) {
68
+ return _jsx(Menu.Label, { children: t('connections.fromDocker.empty') });
69
+ }
70
+ return containers.map((container) => (_jsx(Menu.Item, { onClick: () => onPick(container), children: _jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate", children: composeNameOf(container) ?? container.name }), _jsx("span", { className: "truncate text-xs text-text-faint", children: container.image })] }) }, container.id)));
71
+ }
72
+ /* The button that asks the server who it is, and what it answered; an answer to an older config is not shown. */
73
+ function ConnectionTest({ connection }) {
74
+ const { t } = useTranslation('database');
75
+ const client = useDatabaseClient();
76
+ const [busy, setBusy] = useState(false);
77
+ const [outcome, setOutcome] = useState(null);
78
+ const shown = outcome !== null && outcome.config === connection.config ? outcome : null;
79
+ const test = async () => {
80
+ const config = connection.config;
81
+ setBusy(true);
82
+ try {
83
+ setOutcome({ config, info: await client.test(config) });
84
+ }
85
+ catch (e) {
86
+ setOutcome({ config, error: messageOf(e, t('connections.test.failed')) });
87
+ }
88
+ finally {
89
+ setBusy(false);
90
+ }
91
+ };
92
+ return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("div", { children: _jsxs(Button, { variant: "secondary", disabled: busy || !isValidConfig(connection.config), onClick: () => void test(), children: [busy ? _jsx(Spinner, { size: 14 }) : _jsx(Icon, { icon: Unplug, size: 14 }), t('connections.test.action')] }) }), shown !== null && 'info' in shown && (_jsx(Banner, { icon: CircleCheck, tone: "neutral", className: "w-full", message: t('connections.test.success', { server: `${t(`engine.flavor.${shown.info.flavor}`)} ${shown.info.version}` }) })), shown !== null && 'error' in shown && _jsx(Banner, { icon: CircleAlert, tone: "error", className: "w-full", message: shown.error })] }));
93
+ }
@@ -0,0 +1,9 @@
1
+ import type { Ref } from 'react';
2
+ import type { Engine } from '../protocol/index.ts';
3
+ export interface EngineIconProps {
4
+ engine: Engine;
5
+ size?: number;
6
+ className?: string;
7
+ ref?: Ref<SVGSVGElement>;
8
+ }
9
+ export declare function EngineIcon({ engine, size, className, ref }: EngineIconProps): import("react").JSX.Element;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Icon } from '@adecore/ui';
3
+ import { ENGINE_ICONS } from './engine-icons.js';
4
+ /* A file on disk for SQLite, a server for MySQL and MariaDB. */
5
+ export function EngineIcon({ engine, size = 16, className, ref }) {
6
+ return _jsx(Icon, { ref: ref, icon: ENGINE_ICONS[engine], size: size, className: className });
7
+ }
@@ -0,0 +1,16 @@
1
+ import type { ConnectionConfig, Engine, MysqlConnectionConfig } from '../protocol/index.ts';
2
+ import type { Connection } from '../client/types.ts';
3
+ export declare const DEFAULT_MYSQL_PORT = 3306;
4
+ export declare const DEFAULT_HOST = "127.0.0.1";
5
+ export type ConnectMode = 'tcp' | 'socket' | 'ssh' | 'docker';
6
+ export declare const CONNECT_MODES: readonly ConnectMode[];
7
+ export type ConfigProblem = 'path-required' | 'path-relative' | 'host-required' | 'port-range' | 'socket-required' | 'ssh-host-required' | 'ssh-port-range' | 'container-required' | 'container-port-range';
8
+ export type ConfigProblems = Partial<Record<'path' | 'host' | 'port' | 'socket' | 'sshHost' | 'sshPort' | 'container' | 'containerPort', ConfigProblem>>;
9
+ export declare const defaultConfig: (engine: Engine) => ConnectionConfig;
10
+ export declare const isAbsolutePath: (path: string) => boolean;
11
+ export declare const modeOf: (config: MysqlConnectionConfig) => ConnectMode;
12
+ export declare const configProblems: (config: ConnectionConfig) => ConfigProblems;
13
+ export declare const isValidConfig: (config: ConnectionConfig) => boolean;
14
+ export declare const targetOf: (connection: Connection) => string;
15
+ export declare const withMode: (config: MysqlConnectionConfig, mode: ConnectMode) => MysqlConnectionConfig;
16
+ export declare const withEngine: (connection: Connection, engine: Engine) => Connection;
@@ -0,0 +1,107 @@
1
+ export const DEFAULT_MYSQL_PORT = 3306;
2
+ export const DEFAULT_HOST = '127.0.0.1';
3
+ export const CONNECT_MODES = ['tcp', 'socket', 'ssh', 'docker'];
4
+ /* A config of this engine with every field at what a new connection starts with. */
5
+ export const defaultConfig = (engine) => engine === 'sqlite'
6
+ ? { engine: 'sqlite', path: '', create: false, readOnly: false }
7
+ : { engine: 'mysql', host: DEFAULT_HOST, port: DEFAULT_MYSQL_PORT, user: '', password: '', tls: 'prefer', readOnly: false };
8
+ /* A POSIX path, a Windows drive path or a UNC path. A relative one would resolve against the helper's working directory. */
9
+ export const isAbsolutePath = (path) => /^(?:\/|[A-Za-z]:[\\/]|\\\\)/.test(path);
10
+ const sqliteProblems = (config) => {
11
+ const path = config.path.trim();
12
+ if (path === '') {
13
+ return { path: 'path-required' };
14
+ }
15
+ return isAbsolutePath(path) ? {} : { path: 'path-relative' };
16
+ };
17
+ const isPort = (port) => Number.isInteger(port) && port >= 1 && port <= 65535;
18
+ /* 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. */
19
+ export const modeOf = (config) => {
20
+ if (config.tunnel !== undefined) {
21
+ return config.tunnel.kind;
22
+ }
23
+ return config.socket === undefined ? 'tcp' : 'socket';
24
+ };
25
+ const mysqlProblems = (config) => {
26
+ const problems = {};
27
+ const tunnel = config.tunnel;
28
+ const mode = modeOf(config);
29
+ if (mode === 'socket' && (config.socket ?? '').trim() === '') {
30
+ problems.socket = 'socket-required';
31
+ }
32
+ if ((mode === 'tcp' || mode === 'ssh') && config.host.trim() === '') {
33
+ problems.host = 'host-required';
34
+ }
35
+ if ((mode === 'tcp' || mode === 'ssh') && config.port !== undefined && !isPort(config.port)) {
36
+ problems.port = 'port-range';
37
+ }
38
+ if (tunnel?.kind === 'ssh') {
39
+ if (tunnel.host.trim() === '') {
40
+ problems.sshHost = 'ssh-host-required';
41
+ }
42
+ if (tunnel.port !== undefined && !isPort(tunnel.port)) {
43
+ problems.sshPort = 'ssh-port-range';
44
+ }
45
+ }
46
+ if (tunnel?.kind === 'docker') {
47
+ if (tunnel.container.trim() === '') {
48
+ problems.container = 'container-required';
49
+ }
50
+ if (tunnel.port !== undefined && !isPort(tunnel.port)) {
51
+ problems.containerPort = 'container-port-range';
52
+ }
53
+ }
54
+ return problems;
55
+ };
56
+ export const configProblems = (config) => (config.engine === 'sqlite' ? sqliteProblems(config) : mysqlProblems(config));
57
+ export const isValidConfig = (config) => Object.keys(configProblems(config)).length === 0;
58
+ const lastSegment = (path) => {
59
+ const segments = path.split(/[\\/]/).filter((segment) => segment !== '');
60
+ return segments[segments.length - 1] ?? path;
61
+ };
62
+ const targetWhere = (config) => {
63
+ const tunnel = config.tunnel;
64
+ if (tunnel?.kind === 'docker') {
65
+ return tunnel.container === '' ? 'Docker' : `${tunnel.container} (Docker)`;
66
+ }
67
+ if (tunnel?.kind === 'ssh') {
68
+ return tunnel.host === '' ? config.host : `${config.host} via ${tunnel.host}`;
69
+ }
70
+ if (config.socket !== undefined) {
71
+ return lastSegment(config.socket);
72
+ }
73
+ return `${config.host}:${config.port ?? DEFAULT_MYSQL_PORT}`;
74
+ };
75
+ /* 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)`. */
76
+ export const targetOf = (connection) => {
77
+ const config = connection.config;
78
+ if (config.engine === 'sqlite') {
79
+ return lastSegment(config.path);
80
+ }
81
+ const where = targetWhere(config);
82
+ return config.user === '' || where === '' ? where : `${config.user}@${where}`;
83
+ };
84
+ /* Moves to another way of reaching the server. What the person typed for the user, the password and the database stays. */
85
+ export const withMode = (config, mode) => {
86
+ if (modeOf(config) === mode) {
87
+ return config;
88
+ }
89
+ const base = {
90
+ ...config,
91
+ host: config.host.trim() === '' ? DEFAULT_HOST : config.host,
92
+ socket: undefined,
93
+ tunnel: undefined
94
+ };
95
+ switch (mode) {
96
+ case 'tcp':
97
+ return base;
98
+ case 'socket':
99
+ return { ...base, socket: '' };
100
+ case 'ssh':
101
+ return { ...base, tunnel: { kind: 'ssh', host: '' } };
102
+ case 'docker':
103
+ return { ...base, tunnel: { kind: 'docker', container: '', port: DEFAULT_MYSQL_PORT } };
104
+ }
105
+ };
106
+ /* Moves to another engine, which keeps who the connection is and drops how it connects. */
107
+ export const withEngine = (connection, engine) => connection.config.engine === engine ? connection : { id: connection.id, name: connection.name, config: defaultConfig(engine) };
@@ -0,0 +1,9 @@
1
+ import type { Connection } from '../client/types.ts';
2
+ import type { DockerContainer, MysqlConnectionConfig } from '../protocol/index.ts';
3
+ export declare const isMysqlContainer: (container: DockerContainer) => boolean;
4
+ export declare const composeNameOf: (container: DockerContainer) => string | null;
5
+ export declare const containerTitle: (container: DockerContainer) => string;
6
+ export declare const containerPortOf: (container: DockerContainer) => number;
7
+ export declare const publishedPortOf: (container: DockerContainer, port: number) => number | null;
8
+ export declare const withContainer: (config: MysqlConnectionConfig, container: DockerContainer) => MysqlConnectionConfig;
9
+ export declare const connectionFromContainer: (id: string, container: DockerContainer) => Connection;
@@ -0,0 +1,38 @@
1
+ import { DEFAULT_MYSQL_PORT, defaultConfig, withMode } from './connection-config.js';
2
+ /* A container a MySQL connection can point at: what `discover` found, minus a file engine, which no container serves. */
3
+ export const isMysqlContainer = (container) => container.engine !== 'sqlite';
4
+ /* The Compose `project/service` of a container, when it has both. */
5
+ export const composeNameOf = (container) => container.project !== null && container.service !== null ? `${container.project}/${container.service}` : null;
6
+ /* What names a container to a person: its Compose name, else its own. */
7
+ export const containerTitle = (container) => composeNameOf(container) ?? container.name;
8
+ /* The port to connect to inside the container: MySQL's own when the container lists it or lists nothing, else the first it lists. */
9
+ export const containerPortOf = (container) => {
10
+ const ports = container.ports.map((entry) => entry.container);
11
+ return ports.length === 0 || ports.includes(DEFAULT_MYSQL_PORT) ? DEFAULT_MYSQL_PORT : ports[0];
12
+ };
13
+ /* The host port a container publishes a port on, or `null` when it does not. */
14
+ export const publishedPortOf = (container, port) => container.ports.find((entry) => entry.container === port)?.host ?? null;
15
+ const isEmpty = (text) => text === undefined || text === '';
16
+ /* 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. */
17
+ export const withContainer = (config, container) => {
18
+ const docker = withMode(config, 'docker');
19
+ const { user, password, database } = container.suggested;
20
+ return {
21
+ ...docker,
22
+ user: isEmpty(docker.user) && user !== undefined ? user : docker.user,
23
+ password: isEmpty(docker.password) && password !== undefined ? password : docker.password,
24
+ database: isEmpty(docker.database) && database !== undefined ? database : docker.database,
25
+ tunnel: {
26
+ kind: 'docker',
27
+ container: container.name,
28
+ port: containerPortOf(container),
29
+ ...(docker.tunnel?.kind === 'docker' && docker.tunnel.context !== undefined ? { context: docker.tunnel.context } : {})
30
+ }
31
+ };
32
+ };
33
+ /* A new MySQL connection to a container, named after its Compose `project/service` or its own name. */
34
+ export const connectionFromContainer = (id, container) => ({
35
+ id,
36
+ name: containerTitle(container),
37
+ config: withContainer(defaultConfig('mysql'), container)
38
+ });
@@ -0,0 +1,3 @@
1
+ import { type LucideIcon } from 'lucide-react';
2
+ import type { Engine } from '../protocol/index.ts';
3
+ export declare const ENGINE_ICONS: Record<Engine, LucideIcon>;
@@ -0,0 +1,3 @@
1
+ import { HardDrive, Server } from 'lucide-react';
2
+ /* A file on disk for SQLite, a server for MySQL and MariaDB. */
3
+ export const ENGINE_ICONS = { sqlite: HardDrive, mysql: Server };
@@ -0,0 +1,17 @@
1
+ import type { DockerContainer } from '../protocol/index.ts';
2
+ export type DockerState = {
3
+ readonly status: 'loading';
4
+ } | {
5
+ readonly status: 'ready';
6
+ readonly containers: readonly DockerContainer[];
7
+ } | {
8
+ readonly status: 'unsupported';
9
+ readonly message: string;
10
+ } | {
11
+ readonly status: 'error';
12
+ readonly message: string;
13
+ };
14
+ export declare function useDockerContainers(): {
15
+ readonly state: DockerState;
16
+ reload(): void;
17
+ };
@@ -0,0 +1,26 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { messageOf } from '@adecore/ui';
3
+ import { useDatabaseClient } from '../client-context.js';
4
+ import { DatabaseRequestError } from '../client/types.js';
5
+ /* The running containers that look like a database, asked for when the component mounts and again on `reload`. */
6
+ export function useDockerContainers() {
7
+ const client = useDatabaseClient();
8
+ const [state, setState] = useState({ status: 'loading' });
9
+ const [attempt, setAttempt] = useState(0);
10
+ useEffect(() => {
11
+ const controller = new AbortController();
12
+ client.discover('docker', { signal: controller.signal }).then((containers) => setState({ status: 'ready', containers }), (e) => {
13
+ if (controller.signal.aborted) {
14
+ return;
15
+ }
16
+ const message = messageOf(e);
17
+ setState({ status: e instanceof DatabaseRequestError && e.code === 'unsupported' ? 'unsupported' : 'error', message });
18
+ });
19
+ return () => controller.abort();
20
+ }, [client, attempt]);
21
+ const reload = () => {
22
+ setState({ status: 'loading' });
23
+ setAttempt((current) => current + 1);
24
+ };
25
+ return { state, reload };
26
+ }
@@ -0,0 +1,9 @@
1
+ import type { Ref } from 'react';
2
+ import type { DestructiveStatement } from './destructive.ts';
3
+ export interface DestructiveDialogProps {
4
+ statements: readonly DestructiveStatement[];
5
+ onConfirm(): void;
6
+ onCancel(): void;
7
+ ref?: Ref<HTMLDivElement>;
8
+ }
9
+ export declare function DestructiveDialog({ statements, onConfirm, onCancel, ref }: DestructiveDialogProps): import("react").JSX.Element;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { TriangleAlert } from 'lucide-react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { PromptDialog } from '@adecore/ui';
5
+ /* Lists the statements that cannot be taken back and asks before they run. */
6
+ export function DestructiveDialog({ statements, onConfirm, onCancel, ref }) {
7
+ const { t } = useTranslation('database');
8
+ return (_jsx(PromptDialog, { ref: ref, open: statements.length > 0, danger: true, title: t('console.destructive.title'), titleIcon: TriangleAlert, description: t('console.destructive.description', { count: statements.length }), confirmLabel: t('console.destructive.confirm'), onConfirm: onConfirm, onOpenChange: onCancel, children: _jsx("ul", { className: "mt-3 flex max-h-56 flex-col gap-2 overflow-auto", children: statements.map((statement, index) => (_jsxs("li", { className: "flex min-w-0 flex-col gap-0.5 rounded-md bg-surface-sunken px-2.5 py-1.5", children: [_jsx("span", { className: "text-xs text-status-error", children: t(`console.destructive.${statement.kind}`) }), _jsx("span", { className: "truncate font-mono text-xs text-text", children: statement.sql.replace(/\s+/g, ' ') })] }, `${index}:${statement.sql}`))) }) }));
9
+ }
@@ -0,0 +1,10 @@
1
+ import { type Ref } from 'react';
2
+ import { type HistoryEntry } from './history.ts';
3
+ export interface HistoryPanelProps {
4
+ entries: readonly HistoryEntry[];
5
+ onPick(entry: HistoryEntry, run: boolean): void;
6
+ onClear(): void;
7
+ className?: string;
8
+ ref?: Ref<HTMLElement>;
9
+ }
10
+ export declare function HistoryPanel({ entries, onPick, onClear, className, ref }: HistoryPanelProps): import("react").JSX.Element;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import clsx from 'clsx';
4
+ import { Check, CircleAlert, Search, Trash2 } from 'lucide-react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { Icon, IconButton, Input, isApplePlatform, isModHeld } from '@adecore/ui';
7
+ import { formatMoment, formatNumber } from '@adecore/ui/format';
8
+ import { searchHistory } from './history.js';
9
+ /* The runs of this connection, newest first, with a search over their SQL. */
10
+ export function HistoryPanel({ entries, onPick, onClear, className, ref }) {
11
+ const { t } = useTranslation('database');
12
+ const [query, setQuery] = useState('');
13
+ const shown = searchHistory(entries, query);
14
+ return (_jsxs("aside", { ref: ref, "aria-label": t('console.history.title'), className: clsx('flex w-72 shrink-0 flex-col border-l border-border', className), children: [_jsxs("div", { className: "flex shrink-0 items-center gap-2 p-2", children: [_jsx(Input, { size: "sm", type: "search", icon: Search, className: "min-w-0 flex-1", "aria-label": t('console.history.search'), placeholder: t('console.history.search'), value: query, onChange: (event) => setQuery(event.target.value) }), _jsx(IconButton, { icon: Trash2, size: "sm", label: t('console.history.clear'), disabled: entries.length === 0, onClick: onClear })] }), _jsx("div", { className: "relative min-h-24 flex-1 border-t border-border", children: shown.length === 0 ? (_jsx("p", { className: "p-3 text-xs text-text-muted", children: entries.length === 0 ? t('console.history.empty') : t('console.history.noMatches') })) : (_jsx("ul", { className: "absolute inset-0 overflow-auto", children: shown.map((entry) => (_jsx("li", { className: "border-b border-border-soft", children: _jsxs("button", { type: "button", className: "flex w-full min-w-0 flex-col gap-0.5 px-3 py-2 text-left hover:bg-surface-hover", onClick: (event) => onPick(entry, isModHeld(event, isApplePlatform())), onDoubleClick: () => onPick(entry, true), children: [_jsx("span", { className: "truncate font-mono text-xs text-text", children: entry.sql.replace(/\s+/g, ' ') }), _jsxs("span", { className: "flex items-center gap-1.5 text-xs text-text-muted tabular-nums", children: [_jsx(Icon, { icon: entry.ok ? Check : CircleAlert, size: 12, className: entry.ok ? 'text-positive' : 'text-status-error' }), formatMoment(entry.at), entry.rows !== null && t('console.history.rows', { count: entry.rows, formatted: formatNumber(entry.rows) })] })] }) }, `${entry.at}:${entry.sql}`))) })) }), _jsx("p", { className: "shrink-0 border-t border-border px-3 py-2 text-xs text-text-faint", children: t('console.history.hint') })] }));
15
+ }
@@ -0,0 +1,14 @@
1
+ import { type Ref } from 'react';
2
+ import { type Connection } from '../client/types.ts';
3
+ export interface QueryConsoleProps {
4
+ connection: Connection;
5
+ schema?: string;
6
+ value?: string;
7
+ defaultValue?: string;
8
+ onValueChange?(sql: string): void;
9
+ defaultHistoryOpen?: boolean;
10
+ autoFocus?: boolean;
11
+ className?: string;
12
+ ref?: Ref<HTMLDivElement>;
13
+ }
14
+ export declare function QueryConsole({ connection, schema, value, defaultValue, onValueChange, defaultHistoryOpen, autoFocus, className, ref }: QueryConsoleProps): import("react").JSX.Element;