@rebasepro/studio 0.0.1-canary.09e5ec5

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 (361) hide show
  1. package/LICENSE +114 -0
  2. package/README.md +159 -0
  3. package/dist/ApiExplorer-gMJt5JrS.js +1053 -0
  4. package/dist/ApiExplorer-gMJt5JrS.js.map +1 -0
  5. package/dist/AuthSimulationSelector-BF4rkRGp.js +118 -0
  6. package/dist/AuthSimulationSelector-BF4rkRGp.js.map +1 -0
  7. package/dist/BranchesView-DcHZtvXo.js +292 -0
  8. package/dist/BranchesView-DcHZtvXo.js.map +1 -0
  9. package/dist/CronJobsView-CijCToeK.js +456 -0
  10. package/dist/CronJobsView-CijCToeK.js.map +1 -0
  11. package/dist/JSEditor-D8nVp3Lp.js +1308 -0
  12. package/dist/JSEditor-D8nVp3Lp.js.map +1 -0
  13. package/dist/MonacoEditor-CMYEjiRf.js +161 -0
  14. package/dist/MonacoEditor-CMYEjiRf.js.map +1 -0
  15. package/dist/RLSEditor-DBH09u9v.js +1831 -0
  16. package/dist/RLSEditor-DBH09u9v.js.map +1 -0
  17. package/dist/SQLEditor-CkVx9vgr.js +1792 -0
  18. package/dist/SQLEditor-CkVx9vgr.js.map +1 -0
  19. package/dist/SchemaVisualizer-BgD5Zb77.js +1069 -0
  20. package/dist/SchemaVisualizer-BgD5Zb77.js.map +1 -0
  21. package/dist/StorageView-CTqGFhY9.js +907 -0
  22. package/dist/StorageView-CTqGFhY9.js.map +1 -0
  23. package/dist/common/src/collections/CollectionRegistry.d.ts +56 -0
  24. package/dist/common/src/collections/index.d.ts +1 -0
  25. package/dist/common/src/data/buildRebaseData.d.ts +14 -0
  26. package/dist/common/src/index.d.ts +3 -0
  27. package/dist/common/src/util/builders.d.ts +57 -0
  28. package/dist/common/src/util/callbacks.d.ts +6 -0
  29. package/dist/common/src/util/collections.d.ts +11 -0
  30. package/dist/common/src/util/common.d.ts +2 -0
  31. package/dist/common/src/util/conditions.d.ts +26 -0
  32. package/dist/common/src/util/entities.d.ts +58 -0
  33. package/dist/common/src/util/enums.d.ts +3 -0
  34. package/dist/common/src/util/index.d.ts +16 -0
  35. package/dist/common/src/util/navigation_from_path.d.ts +34 -0
  36. package/dist/common/src/util/navigation_utils.d.ts +20 -0
  37. package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
  38. package/dist/common/src/util/paths.d.ts +14 -0
  39. package/dist/common/src/util/permissions.d.ts +5 -0
  40. package/dist/common/src/util/references.d.ts +2 -0
  41. package/dist/common/src/util/relations.d.ts +22 -0
  42. package/dist/common/src/util/resolutions.d.ts +72 -0
  43. package/dist/common/src/util/storage.d.ts +24 -0
  44. package/dist/core/src/components/AIIcon.d.ts +16 -0
  45. package/dist/core/src/components/ConfirmationDialog.d.ts +9 -0
  46. package/dist/core/src/components/Debug/UIReferenceView.d.ts +1 -0
  47. package/dist/core/src/components/Debug/UIStyleGuide.d.ts +1 -0
  48. package/dist/core/src/components/ErrorTooltip.d.ts +2 -0
  49. package/dist/core/src/components/ErrorView.d.ts +21 -0
  50. package/dist/core/src/components/LanguageToggle.d.ts +1 -0
  51. package/dist/core/src/components/LoginView/LoginView.d.ts +68 -0
  52. package/dist/core/src/components/LoginView/index.d.ts +2 -0
  53. package/dist/core/src/components/NotFoundPage.d.ts +1 -0
  54. package/dist/core/src/components/RebaseAuth.d.ts +10 -0
  55. package/dist/core/src/components/RebaseLogo.d.ts +7 -0
  56. package/dist/core/src/components/UnsavedChangesDialog.d.ts +9 -0
  57. package/dist/core/src/components/UserDisplay.d.ts +7 -0
  58. package/dist/core/src/components/UserSelectPopover.d.ts +62 -0
  59. package/dist/core/src/components/UserSettingsView.d.ts +1 -0
  60. package/dist/core/src/components/common/index.d.ts +6 -0
  61. package/dist/core/src/components/common/table_height.d.ts +5 -0
  62. package/dist/core/src/components/common/types.d.ts +63 -0
  63. package/dist/core/src/components/common/useColumnsIds.d.ts +9 -0
  64. package/dist/core/src/components/common/useDataTableController.d.ts +45 -0
  65. package/dist/core/src/components/common/useDebouncedData.d.ts +9 -0
  66. package/dist/core/src/components/common/useScrollRestoration.d.ts +14 -0
  67. package/dist/core/src/components/index.d.ts +16 -0
  68. package/dist/core/src/contexts/AdminModeController.d.ts +4 -0
  69. package/dist/core/src/contexts/AnalyticsContext.d.ts +3 -0
  70. package/dist/core/src/contexts/AuthControllerContext.d.ts +3 -0
  71. package/dist/core/src/contexts/CustomizationControllerContext.d.ts +3 -0
  72. package/dist/core/src/contexts/DataDriverContext.d.ts +3 -0
  73. package/dist/core/src/contexts/DatabaseAdminContext.d.ts +3 -0
  74. package/dist/core/src/contexts/DialogsProvider.d.ts +4 -0
  75. package/dist/core/src/contexts/EffectiveRoleController.d.ts +4 -0
  76. package/dist/core/src/contexts/InternalUserManagementContext.d.ts +3 -0
  77. package/dist/core/src/contexts/ModeController.d.ts +4 -0
  78. package/dist/core/src/contexts/RebaseClientInstanceContext.d.ts +6 -0
  79. package/dist/core/src/contexts/RebaseDataContext.d.ts +3 -0
  80. package/dist/core/src/contexts/SnackbarProvider.d.ts +2 -0
  81. package/dist/core/src/contexts/StorageSourceContext.d.ts +3 -0
  82. package/dist/core/src/contexts/UserConfigurationPersistenceContext.d.ts +3 -0
  83. package/dist/core/src/contexts/index.d.ts +13 -0
  84. package/dist/core/src/core/PluginLifecycleManager.d.ts +17 -0
  85. package/dist/core/src/core/PluginProviderStack.d.ts +21 -0
  86. package/dist/core/src/core/Rebase.d.ts +14 -0
  87. package/dist/core/src/core/RebaseProps.d.ts +136 -0
  88. package/dist/core/src/core/RebaseRouter.d.ts +4 -0
  89. package/dist/core/src/core/RebaseRoutes.d.ts +17 -0
  90. package/dist/core/src/core/index.d.ts +4 -0
  91. package/dist/core/src/hooks/ApiConfigContext.d.ts +24 -0
  92. package/dist/core/src/hooks/data/delete.d.ts +31 -0
  93. package/dist/core/src/hooks/data/save.d.ts +34 -0
  94. package/dist/core/src/hooks/data/useCollectionFetch.d.ts +51 -0
  95. package/dist/core/src/hooks/data/useData.d.ts +13 -0
  96. package/dist/core/src/hooks/data/useDataOrder.d.ts +12 -0
  97. package/dist/core/src/hooks/data/useEntityFetch.d.ts +38 -0
  98. package/dist/core/src/hooks/data/useRelationSelector.d.ts +52 -0
  99. package/dist/core/src/hooks/data/useUserSelector.d.ts +31 -0
  100. package/dist/core/src/hooks/index.d.ts +37 -0
  101. package/dist/core/src/hooks/useAdminModeController.d.ts +19 -0
  102. package/dist/core/src/hooks/useAnalyticsController.d.ts +5 -0
  103. package/dist/core/src/hooks/useAuthController.d.ts +11 -0
  104. package/dist/core/src/hooks/useAuthSubscription.d.ts +2 -0
  105. package/dist/core/src/hooks/useBackendStorageSource.d.ts +30 -0
  106. package/dist/core/src/hooks/useBridgeRegistration.d.ts +18 -0
  107. package/dist/core/src/hooks/useBrowserTitleAndIcon.d.ts +6 -0
  108. package/dist/core/src/hooks/useBuildAdminModeController.d.ts +6 -0
  109. package/dist/core/src/hooks/useBuildEffectiveRoleController.d.ts +8 -0
  110. package/dist/core/src/hooks/useBuildLocalConfigurationPersistence.d.ts +2 -0
  111. package/dist/core/src/hooks/useBuildModeController.d.ts +6 -0
  112. package/dist/core/src/hooks/useClipboard.d.ts +57 -0
  113. package/dist/core/src/hooks/useCollapsedGroups.d.ts +12 -0
  114. package/dist/core/src/hooks/useCustomizationController.d.ts +11 -0
  115. package/dist/core/src/hooks/useDialogsController.d.ts +11 -0
  116. package/dist/core/src/hooks/useEffectiveRoleController.d.ts +7 -0
  117. package/dist/core/src/hooks/useInternalUserManagementController.d.ts +12 -0
  118. package/dist/core/src/hooks/useLargeLayout.d.ts +1 -0
  119. package/dist/core/src/hooks/useModeController.d.ts +19 -0
  120. package/dist/core/src/hooks/usePermissions.d.ts +12 -0
  121. package/dist/core/src/hooks/useRebaseClient.d.ts +5 -0
  122. package/dist/core/src/hooks/useRebaseContext.d.ts +11 -0
  123. package/dist/core/src/hooks/useRebaseRegistry.d.ts +34 -0
  124. package/dist/core/src/hooks/useSlot.d.ts +18 -0
  125. package/dist/core/src/hooks/useSnackbarController.d.ts +20 -0
  126. package/dist/core/src/hooks/useStorageSource.d.ts +7 -0
  127. package/dist/core/src/hooks/useStudioBridge.d.ts +91 -0
  128. package/dist/core/src/hooks/useTranslation.d.ts +17 -0
  129. package/dist/core/src/hooks/useUnsavedChangesDialog.d.ts +12 -0
  130. package/dist/core/src/hooks/useUserConfigurationPersistence.d.ts +8 -0
  131. package/dist/core/src/hooks/useValidateAuthenticator.d.ts +21 -0
  132. package/dist/core/src/i18n/RebaseI18nProvider.d.ts +33 -0
  133. package/dist/core/src/index.d.ts +15 -0
  134. package/dist/core/src/internal/common.d.ts +3 -0
  135. package/dist/core/src/internal/useRestoreScroll.d.ts +6 -0
  136. package/dist/core/src/locales/de.d.ts +2 -0
  137. package/dist/core/src/locales/en.d.ts +10 -0
  138. package/dist/core/src/locales/es.d.ts +10 -0
  139. package/dist/core/src/locales/fr.d.ts +2 -0
  140. package/dist/core/src/locales/hi.d.ts +2 -0
  141. package/dist/core/src/locales/it.d.ts +2 -0
  142. package/dist/core/src/locales/pt.d.ts +7 -0
  143. package/dist/core/src/util/constants.d.ts +1 -0
  144. package/dist/core/src/util/createFormexStub.d.ts +2 -0
  145. package/dist/core/src/util/entity_cache.d.ts +27 -0
  146. package/dist/core/src/util/enums.d.ts +5 -0
  147. package/dist/core/src/util/icon_list.d.ts +5 -0
  148. package/dist/core/src/util/icon_synonyms.d.ts +1 -0
  149. package/dist/core/src/util/icons.d.ts +20 -0
  150. package/dist/core/src/util/index.d.ts +10 -0
  151. package/dist/core/src/util/previews.d.ts +4 -0
  152. package/dist/core/src/util/useStorageUploadController.d.ts +38 -0
  153. package/dist/core/src/util/useTraceUpdate.d.ts +2 -0
  154. package/dist/formex/src/Field.d.ts +52 -0
  155. package/dist/formex/src/Formex.d.ts +7 -0
  156. package/dist/formex/src/index.d.ts +5 -0
  157. package/dist/formex/src/types.d.ts +40 -0
  158. package/dist/formex/src/useCreateFormex.d.ts +14 -0
  159. package/dist/formex/src/utils.d.ts +16 -0
  160. package/dist/index.es.js +726 -0
  161. package/dist/index.es.js.map +1 -0
  162. package/dist/index.umd.js +9647 -0
  163. package/dist/index.umd.js.map +1 -0
  164. package/dist/studio/src/components/ApiExplorer/ApiExplorer.d.ts +9 -0
  165. package/dist/studio/src/components/ApiExplorer/EndpointDetail.d.ts +9 -0
  166. package/dist/studio/src/components/ApiExplorer/TryItPanel.d.ts +15 -0
  167. package/dist/studio/src/components/ApiExplorer/parseSpec.d.ts +16 -0
  168. package/dist/studio/src/components/ApiExplorer/types.d.ts +90 -0
  169. package/dist/studio/src/components/AuthSimulationSelector.d.ts +11 -0
  170. package/dist/studio/src/components/Branches/BranchesView.d.ts +1 -0
  171. package/dist/studio/src/components/CronJobs/CronJobsView.d.ts +1 -0
  172. package/dist/studio/src/components/JSEditor/JSEditor.d.ts +1 -0
  173. package/dist/studio/src/components/JSEditor/JSEditorSidebar.d.ts +21 -0
  174. package/dist/studio/src/components/JSEditor/JSMonacoEditor.d.ts +18 -0
  175. package/dist/studio/src/components/RLSEditor/PolicyEditor.d.ts +9 -0
  176. package/dist/studio/src/components/RLSEditor/RLSEditor.d.ts +19 -0
  177. package/dist/studio/src/components/RLSEditor/index.d.ts +1 -0
  178. package/dist/studio/src/components/RebaseStudio.d.ts +2 -0
  179. package/dist/studio/src/components/SQLEditor/ExplainVisualizer.d.ts +24 -0
  180. package/dist/studio/src/components/SQLEditor/MonacoEditor.d.ts +17 -0
  181. package/dist/studio/src/components/SQLEditor/SQLEditor.d.ts +11 -0
  182. package/dist/studio/src/components/SQLEditor/SQLEditorSidebar.d.ts +21 -0
  183. package/dist/studio/src/components/SQLEditor/SchemaBrowser.d.ts +8 -0
  184. package/dist/studio/src/components/SchemaVisualizer/RelationEdge.d.ts +3 -0
  185. package/dist/studio/src/components/SchemaVisualizer/SchemaVisualizer.d.ts +2 -0
  186. package/dist/studio/src/components/SchemaVisualizer/TableNode.d.ts +3 -0
  187. package/dist/studio/src/components/SchemaVisualizer/index.d.ts +5 -0
  188. package/dist/studio/src/components/SchemaVisualizer/schema-visualizer.utils.d.ts +42 -0
  189. package/dist/studio/src/components/SchemaVisualizer/useSchemaGraph.d.ts +37 -0
  190. package/dist/studio/src/components/StorageView/StorageView.d.ts +1 -0
  191. package/dist/studio/src/components/StudioHomePage.d.ts +9 -0
  192. package/dist/studio/src/index.d.ts +4 -0
  193. package/dist/studio/src/utils/entities.d.ts +0 -0
  194. package/dist/studio/src/utils/pgColumnToProperty.d.ts +6 -0
  195. package/dist/studio/src/utils/sql_utils.d.ts +52 -0
  196. package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
  197. package/dist/types/src/controllers/auth.d.ts +119 -0
  198. package/dist/types/src/controllers/client.d.ts +170 -0
  199. package/dist/types/src/controllers/collection_registry.d.ts +45 -0
  200. package/dist/types/src/controllers/customization_controller.d.ts +60 -0
  201. package/dist/types/src/controllers/data.d.ts +168 -0
  202. package/dist/types/src/controllers/data_driver.d.ts +160 -0
  203. package/dist/types/src/controllers/database_admin.d.ts +11 -0
  204. package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
  205. package/dist/types/src/controllers/effective_role.d.ts +4 -0
  206. package/dist/types/src/controllers/email.d.ts +34 -0
  207. package/dist/types/src/controllers/index.d.ts +18 -0
  208. package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
  209. package/dist/types/src/controllers/navigation.d.ts +213 -0
  210. package/dist/types/src/controllers/registry.d.ts +54 -0
  211. package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
  212. package/dist/types/src/controllers/side_entity_controller.d.ts +90 -0
  213. package/dist/types/src/controllers/snackbar.d.ts +24 -0
  214. package/dist/types/src/controllers/storage.d.ts +171 -0
  215. package/dist/types/src/index.d.ts +4 -0
  216. package/dist/types/src/rebase_context.d.ts +105 -0
  217. package/dist/types/src/types/backend.d.ts +536 -0
  218. package/dist/types/src/types/builders.d.ts +15 -0
  219. package/dist/types/src/types/chips.d.ts +5 -0
  220. package/dist/types/src/types/collections.d.ts +856 -0
  221. package/dist/types/src/types/cron.d.ts +102 -0
  222. package/dist/types/src/types/data_source.d.ts +64 -0
  223. package/dist/types/src/types/entities.d.ts +145 -0
  224. package/dist/types/src/types/entity_actions.d.ts +98 -0
  225. package/dist/types/src/types/entity_callbacks.d.ts +173 -0
  226. package/dist/types/src/types/entity_link_builder.d.ts +7 -0
  227. package/dist/types/src/types/entity_overrides.d.ts +10 -0
  228. package/dist/types/src/types/entity_views.d.ts +61 -0
  229. package/dist/types/src/types/export_import.d.ts +21 -0
  230. package/dist/types/src/types/index.d.ts +23 -0
  231. package/dist/types/src/types/locales.d.ts +4 -0
  232. package/dist/types/src/types/modify_collections.d.ts +5 -0
  233. package/dist/types/src/types/plugins.d.ts +279 -0
  234. package/dist/types/src/types/properties.d.ts +1176 -0
  235. package/dist/types/src/types/property_config.d.ts +70 -0
  236. package/dist/types/src/types/relations.d.ts +336 -0
  237. package/dist/types/src/types/slots.d.ts +252 -0
  238. package/dist/types/src/types/translations.d.ts +870 -0
  239. package/dist/types/src/types/user_management_delegate.d.ts +121 -0
  240. package/dist/types/src/types/websockets.d.ts +78 -0
  241. package/dist/types/src/users/index.d.ts +2 -0
  242. package/dist/types/src/users/roles.d.ts +22 -0
  243. package/dist/types/src/users/user.d.ts +46 -0
  244. package/dist/ui/src/components/Alert.d.ts +12 -0
  245. package/dist/ui/src/components/Autocomplete.d.ts +21 -0
  246. package/dist/ui/src/components/Avatar.d.ts +11 -0
  247. package/dist/ui/src/components/Badge.d.ts +8 -0
  248. package/dist/ui/src/components/BooleanSwitch.d.ts +14 -0
  249. package/dist/ui/src/components/BooleanSwitchWithLabel.d.ts +17 -0
  250. package/dist/ui/src/components/Button.d.ts +14 -0
  251. package/dist/ui/src/components/Card.d.ts +9 -0
  252. package/dist/ui/src/components/CenteredView.d.ts +9 -0
  253. package/dist/ui/src/components/Checkbox.d.ts +13 -0
  254. package/dist/ui/src/components/Chip.d.ts +26 -0
  255. package/dist/ui/src/components/CircularProgress.d.ts +5 -0
  256. package/dist/ui/src/components/CircularProgressCenter.d.ts +11 -0
  257. package/dist/ui/src/components/Collapse.d.ts +9 -0
  258. package/dist/ui/src/components/ColorPicker.d.ts +30 -0
  259. package/dist/ui/src/components/Container.d.ts +8 -0
  260. package/dist/ui/src/components/DateTimeField.d.ts +24 -0
  261. package/dist/ui/src/components/DebouncedTextField.d.ts +2 -0
  262. package/dist/ui/src/components/Dialog.d.ts +39 -0
  263. package/dist/ui/src/components/DialogActions.d.ts +7 -0
  264. package/dist/ui/src/components/DialogContent.d.ts +7 -0
  265. package/dist/ui/src/components/DialogTitle.d.ts +10 -0
  266. package/dist/ui/src/components/ErrorBoundary.d.ts +11 -0
  267. package/dist/ui/src/components/ExpandablePanel.d.ts +12 -0
  268. package/dist/ui/src/components/FileUpload.d.ts +23 -0
  269. package/dist/ui/src/components/IconButton.d.ts +12 -0
  270. package/dist/ui/src/components/InfoLabel.d.ts +5 -0
  271. package/dist/ui/src/components/InputLabel.d.ts +11 -0
  272. package/dist/ui/src/components/Label.d.ts +7 -0
  273. package/dist/ui/src/components/LoadingButton.d.ts +7 -0
  274. package/dist/ui/src/components/Markdown.d.ts +10 -0
  275. package/dist/ui/src/components/Menu.d.ts +23 -0
  276. package/dist/ui/src/components/Menubar.d.ts +80 -0
  277. package/dist/ui/src/components/MultiSelect.d.ts +48 -0
  278. package/dist/ui/src/components/Paper.d.ts +6 -0
  279. package/dist/ui/src/components/Popover.d.ts +24 -0
  280. package/dist/ui/src/components/RadioGroup.d.ts +28 -0
  281. package/dist/ui/src/components/ResizablePanels.d.ts +18 -0
  282. package/dist/ui/src/components/SearchBar.d.ts +22 -0
  283. package/dist/ui/src/components/Select.d.ts +43 -0
  284. package/dist/ui/src/components/Separator.d.ts +5 -0
  285. package/dist/ui/src/components/Sheet.d.ts +22 -0
  286. package/dist/ui/src/components/Skeleton.d.ts +6 -0
  287. package/dist/ui/src/components/Slider.d.ts +21 -0
  288. package/dist/ui/src/components/Table.d.ts +34 -0
  289. package/dist/ui/src/components/Tabs.d.ts +19 -0
  290. package/dist/ui/src/components/TextField.d.ts +58 -0
  291. package/dist/ui/src/components/TextareaAutosize.d.ts +43 -0
  292. package/dist/ui/src/components/ToggleButtonGroup.d.ts +30 -0
  293. package/dist/ui/src/components/Tooltip.d.ts +19 -0
  294. package/dist/ui/src/components/Typography.d.ts +36 -0
  295. package/dist/ui/src/components/VirtualTable/VirtualTable.d.ts +11 -0
  296. package/dist/ui/src/components/VirtualTable/VirtualTableCell.d.ts +21 -0
  297. package/dist/ui/src/components/VirtualTable/VirtualTableHeader.d.ts +29 -0
  298. package/dist/ui/src/components/VirtualTable/VirtualTableHeaderRow.d.ts +2 -0
  299. package/dist/ui/src/components/VirtualTable/VirtualTableProps.d.ts +243 -0
  300. package/dist/ui/src/components/VirtualTable/VirtualTableRow.d.ts +3 -0
  301. package/dist/ui/src/components/VirtualTable/index.d.ts +3 -0
  302. package/dist/ui/src/components/VirtualTable/types.d.ts +38 -0
  303. package/dist/ui/src/components/common/SelectInputLabel.d.ts +5 -0
  304. package/dist/ui/src/components/index.d.ts +53 -0
  305. package/dist/ui/src/hooks/PortalContainerContext.d.ts +31 -0
  306. package/dist/ui/src/hooks/index.d.ts +6 -0
  307. package/dist/ui/src/hooks/useDebounceCallback.d.ts +1 -0
  308. package/dist/ui/src/hooks/useDebounceValue.d.ts +1 -0
  309. package/dist/ui/src/hooks/useDebouncedCallback.d.ts +1 -0
  310. package/dist/ui/src/hooks/useInjectStyles.d.ts +7 -0
  311. package/dist/ui/src/hooks/useOutsideAlerter.d.ts +5 -0
  312. package/dist/ui/src/icons/GitHubIcon.d.ts +2 -0
  313. package/dist/ui/src/icons/HandleIcon.d.ts +1 -0
  314. package/dist/ui/src/icons/Icon.d.ts +20 -0
  315. package/dist/ui/src/icons/cool_icon_keys.d.ts +1 -0
  316. package/dist/ui/src/icons/icon_keys.d.ts +1 -0
  317. package/dist/ui/src/icons/index.d.ts +6 -0
  318. package/dist/ui/src/index.d.ts +5 -0
  319. package/dist/ui/src/styles.d.ts +12 -0
  320. package/dist/ui/src/util/chip_colors.d.ts +4 -0
  321. package/dist/ui/src/util/cls.d.ts +2 -0
  322. package/dist/ui/src/util/debounce.d.ts +10 -0
  323. package/dist/ui/src/util/hash.d.ts +1 -0
  324. package/dist/ui/src/util/index.d.ts +4 -0
  325. package/dist/ui/src/util/key_to_icon_component.d.ts +1 -0
  326. package/package.json +84 -0
  327. package/src/components/ApiExplorer/ApiExplorer.tsx +290 -0
  328. package/src/components/ApiExplorer/EndpointDetail.tsx +271 -0
  329. package/src/components/ApiExplorer/TryItPanel.tsx +510 -0
  330. package/src/components/ApiExplorer/parseSpec.ts +104 -0
  331. package/src/components/ApiExplorer/types.ts +84 -0
  332. package/src/components/AuthSimulationSelector.tsx +77 -0
  333. package/src/components/Branches/BranchesView.tsx +370 -0
  334. package/src/components/CronJobs/CronJobsView.tsx +346 -0
  335. package/src/components/JSEditor/JSEditor.tsx +1033 -0
  336. package/src/components/JSEditor/JSEditorSidebar.tsx +340 -0
  337. package/src/components/JSEditor/JSMonacoEditor.tsx +390 -0
  338. package/src/components/RLSEditor/PolicyEditor.tsx +444 -0
  339. package/src/components/RLSEditor/RLSEditor.tsx +692 -0
  340. package/src/components/RLSEditor/index.ts +1 -0
  341. package/src/components/RebaseStudio.tsx +121 -0
  342. package/src/components/SQLEditor/ExplainVisualizer.tsx +128 -0
  343. package/src/components/SQLEditor/MonacoEditor.tsx +203 -0
  344. package/src/components/SQLEditor/SQLEditor.tsx +1419 -0
  345. package/src/components/SQLEditor/SQLEditorSidebar.tsx +174 -0
  346. package/src/components/SQLEditor/SchemaBrowser.tsx +158 -0
  347. package/src/components/SchemaVisualizer/RelationEdge.tsx +102 -0
  348. package/src/components/SchemaVisualizer/SchemaVisualizer.tsx +665 -0
  349. package/src/components/SchemaVisualizer/TableNode.tsx +257 -0
  350. package/src/components/SchemaVisualizer/index.ts +5 -0
  351. package/src/components/SchemaVisualizer/schema-visualizer.utils.ts +140 -0
  352. package/src/components/SchemaVisualizer/useSchemaGraph.ts +397 -0
  353. package/src/components/StorageView/StorageView.tsx +1035 -0
  354. package/src/components/StudioHomePage.tsx +357 -0
  355. package/src/index.ts +31 -0
  356. package/src/utils/entities.ts +2 -0
  357. package/src/utils/pgColumnToProperty.test.ts +401 -0
  358. package/src/utils/pgColumnToProperty.ts +275 -0
  359. package/src/utils/sql_utils.test.ts +265 -0
  360. package/src/utils/sql_utils.ts +291 -0
  361. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,1419 @@
1
+
2
+ import { IconForView } from "@rebasepro/core";
3
+ import { useStudioCollectionRegistry, useStudioSideEntityController } from "@rebasepro/core";
4
+ import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
5
+ import { createPortal } from "react-dom";
6
+ import { Button, Paper, Typography, CircularProgress, cls, IconButton, InputLabel, Dialog, DialogContent, DialogActions, DialogTitle, TextField, Tooltip, Alert, Tabs, Tab, defaultBorderMixin, Select, SelectItem, Menu, MenuItem, ResizablePanels, Chip, VirtualTable, VirtualTableColumn , iconSize } from "@rebasepro/ui";
7
+ import { DatabaseIcon, TerminalIcon, XIcon, PlusIcon, PencilIcon, MoreVerticalIcon, MenuIcon, PlayIcon } from "lucide-react";
8
+ // VirtualTableInput is conditionally loaded from CMS when available
9
+ let VirtualTableInput: React.ComponentType<any> | null = null;
10
+ try {
11
+ // @ts-ignore — optional peer dependency
12
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
13
+ const cms = require("@rebasepro/admin");
14
+ VirtualTableInput = cms.VirtualTableInput;
15
+ } catch { /* CMS not available */ }
16
+ import { useRebaseContext, useSnackbarController, ConfirmationDialog, ErrorView, useTranslation } from "@rebasepro/core";
17
+ import { MonacoEditor } from "./MonacoEditor";
18
+ import { SQLEditorSidebar, Snippet } from "./SQLEditorSidebar";
19
+ import { parseFirst } from "pgsql-ast-parser";
20
+ import { determineTableAndPK, resolveQueryCollections, ResolvedQueryCollection } from "../../utils/sql_utils";
21
+ import { ExplainVisualizer } from "./ExplainVisualizer";
22
+
23
+ export interface SQLEditorColumnInfo {
24
+ name: string;
25
+ dataType: string;
26
+ isPrimaryKey: boolean;
27
+ }
28
+
29
+ export interface TableInfo {
30
+ schemaName: string;
31
+ tableName: string;
32
+ columns: SQLEditorColumnInfo[];
33
+ }
34
+
35
+ const QueryLoadingView = () => {
36
+ const [elapsed, setElapsed] = useState(0);
37
+
38
+ useEffect(() => {
39
+ const start = Date.now();
40
+ const interval = setInterval(() => {
41
+ setElapsed(Date.now() - start);
42
+ }, 100);
43
+ return () => clearInterval(interval);
44
+ }, []);
45
+
46
+ return (
47
+ <div className="flex-grow flex items-center justify-center">
48
+ <div className="text-center">
49
+ <CircularProgress size="medium"/>
50
+ <Typography variant="body2" className="mt-4 text-text-secondary dark:text-text-secondary-dark font-mono tracking-tight animate-pulse">
51
+ EXECUTING QUERY...
52
+ </Typography>
53
+ <div className="mt-2 text-xs font-mono text-text-disabled dark:text-text-disabled-dark">
54
+ {(elapsed / 1000).toFixed(1)}s elapsed
55
+ </div>
56
+ </div>
57
+ </div>
58
+ );
59
+ };
60
+
61
+ const STORAGE_KEY_TABS = "rebase_sql_tabs";
62
+ const STORAGE_KEY_ACTIVE_TAB = "rebase_sql_active_tab";
63
+
64
+ const FixedEditorOverlay = ({
65
+ displayValue,
66
+ onSave,
67
+ onCancel
68
+ }: {
69
+ displayValue: string,
70
+ onSave: (val: string | null) => void,
71
+ onCancel: () => void
72
+ }) => {
73
+ const [rect, setRect] = useState<DOMRect | null>(null);
74
+ const [windowSize, setWindowSize] = useState({ width: 1000, height: 1000 });
75
+ const anchorRef = useRef<HTMLDivElement>(null);
76
+
77
+ useEffect(() => {
78
+ if (anchorRef.current && anchorRef.current.parentElement) {
79
+ setRect(anchorRef.current.parentElement.getBoundingClientRect());
80
+ }
81
+ if (typeof window !== "undefined") {
82
+ setWindowSize({ width: window.innerWidth, height: window.innerHeight });
83
+ const handleResize = () => setWindowSize({ width: window.innerWidth, height: window.innerHeight });
84
+ window.addEventListener('resize', handleResize);
85
+ return () => window.removeEventListener('resize', handleResize);
86
+ }
87
+ return undefined;
88
+ }, []);
89
+
90
+ if (!rect) {
91
+ return <div ref={anchorRef} className="w-full h-full min-h-[20px]" />;
92
+ }
93
+
94
+ let top = rect.top - 2;
95
+ let left = rect.left - 2;
96
+ const minWidth = Math.max(rect.width + 4, 250);
97
+ const minHeight = rect.height + 4;
98
+
99
+ if (left + minWidth > windowSize.width) {
100
+ left = Math.max(10, windowSize.width - minWidth - 10);
101
+ }
102
+
103
+ // Calculate a max height that doesn't overflow the bottom
104
+ const maxAvailableHeight = Math.max(50, windowSize.height - top - 10);
105
+ const resolvedMaxHeight = Math.min(300, maxAvailableHeight);
106
+
107
+ // If even the min height overflows, adjust top
108
+ if (top + minHeight > windowSize.height) {
109
+ top = Math.max(10, windowSize.height - minHeight - 10);
110
+ }
111
+
112
+ return (
113
+ <div ref={anchorRef} className="w-full h-full min-h-[20px]">
114
+ {createPortal(
115
+ <div
116
+ className="fixed z-[9999] bg-surface-50 dark:bg-surface-900 border-2 border-primary dark:border-primary-dark shadow-xl flex flex-col"
117
+ style={{
118
+ top,
119
+ left,
120
+ minWidth,
121
+ minHeight,
122
+ maxWidth: Math.min(400, windowSize.width - left - 10)
123
+ }}
124
+ >
125
+ <textarea
126
+ className="w-full h-full bg-transparent outline-none border-none ring-0 font-mono text-[13px] text-text-primary dark:text-text-primary-dark px-4 py-1.5 resize-none overflow-y-auto"
127
+ defaultValue={displayValue}
128
+ autoFocus
129
+ style={{ minHeight: '32px' }}
130
+ onFocus={(e) => {
131
+ const val = e.target.value;
132
+ e.target.value = "";
133
+ e.target.value = val;
134
+ e.target.style.height = 'auto';
135
+ e.target.style.height = `${Math.min(e.target.scrollHeight, resolvedMaxHeight)}px`;
136
+ }}
137
+ onChange={(e) => {
138
+ e.target.style.height = 'auto';
139
+ e.target.style.height = `${Math.min(e.target.scrollHeight, resolvedMaxHeight)}px`;
140
+ }}
141
+ onBlur={(e) => {
142
+ onSave(e.target.value || null);
143
+ onCancel();
144
+ }}
145
+ onKeyDown={(e) => {
146
+ if (e.key === "Enter" && !e.shiftKey) {
147
+ e.preventDefault();
148
+ onSave((e.currentTarget as HTMLTextAreaElement).value || null);
149
+ onCancel();
150
+ }
151
+ if (e.key === "Escape") onCancel();
152
+ }}
153
+ />
154
+ </div>,
155
+ document.body
156
+ )}
157
+ </div>
158
+ );
159
+ };
160
+
161
+ export const SQLEditor = () => {
162
+ const { databaseAdmin } = useRebaseContext();
163
+ const sideEntityController = useStudioSideEntityController();
164
+ const snackbarController = useSnackbarController();
165
+ const collectionRegistry = useStudioCollectionRegistry();
166
+
167
+ const { t } = useTranslation();
168
+
169
+ // Schema state
170
+ const [schemas, setSchemas] = useState<Record<string, TableInfo[]>>({});
171
+ const [isSchemaLoading, setIsSchemaLoading] = useState(true);
172
+ const schemaFetchedRef = useRef(false);
173
+ const [schemaError, setSchemaError] = useState<string | null>(null);
174
+
175
+ // Connection state
176
+ const [selectedDatabase, setSelectedDatabase] = useState<string | undefined>(() => {
177
+ return localStorage.getItem("rebase_sql_selected_db") || undefined;
178
+ });
179
+ const [selectedRole, setSelectedRole] = useState<string | undefined>(() => {
180
+ return localStorage.getItem("rebase_sql_selected_role") || undefined;
181
+ });
182
+
183
+ const [availableDatabases, setAvailableDatabases] = useState<string[]>([]);
184
+ const [availableRoles, setAvailableRoles] = useState<string[]>([]);
185
+ const [isLoadingConfig, setIsLoadingConfig] = useState(true);
186
+ const [connectionConfigError, setConnectionConfigError] = useState<string | null>(null);
187
+
188
+ // Tabbed interface state
189
+ const [tabs, setTabs] = useState<Array<{
190
+ id: string,
191
+ name: string,
192
+ sql: string,
193
+ database?: string,
194
+ role?: string,
195
+ results: Record<string, unknown>[] | null,
196
+ loading: boolean,
197
+ error: string | null,
198
+ execTime: number | null,
199
+ lastExecutedSql: string | null
200
+ }>>(() => {
201
+ const saved = localStorage.getItem(STORAGE_KEY_TABS);
202
+ if (saved) {
203
+ const parsed = JSON.parse(saved);
204
+ return parsed.map((t: Record<string, unknown>) => ({
205
+ ...t,
206
+ results: null,
207
+ loading: false,
208
+ error: null,
209
+ execTime: null,
210
+ lastExecutedSql: null
211
+ }));
212
+ }
213
+ return [{
214
+ id: "1",
215
+ name: "Query 1",
216
+ sql: "SELECT * FROM ",
217
+ database: localStorage.getItem("rebase_sql_selected_db") || undefined,
218
+ role: localStorage.getItem("rebase_sql_selected_role") || undefined,
219
+ results: null,
220
+ loading: false,
221
+ error: null,
222
+ execTime: null,
223
+ lastExecutedSql: null
224
+ }];
225
+ });
226
+ const [activeTabId, setActiveTabId] = useState<string>(() => {
227
+ return localStorage.getItem(STORAGE_KEY_ACTIVE_TAB) || "1";
228
+ });
229
+
230
+ const activeTab = tabs.find(t => t.id === activeTabId) || tabs[0];
231
+
232
+ // Helper to update active tab state
233
+ const updateActiveTab = useCallback((update: Partial<typeof activeTab>) => {
234
+ setTabs(prev => prev.map(t => t.id === activeTabId ? { ...t,
235
+ ...update } : t));
236
+ }, [activeTabId]);
237
+
238
+ const sql = activeTab.sql;
239
+ const results = activeTab.results;
240
+ const loading = activeTab.loading;
241
+ const error = activeTab.error;
242
+ const execTime = activeTab.execTime;
243
+
244
+ const setSql = (newSql: string) => updateActiveTab({ sql: newSql });
245
+ const setResults = (newResults: Record<string, unknown>[] | null) => updateActiveTab({ results: newResults });
246
+ const setLoading = (newLoading: boolean) => updateActiveTab({ loading: newLoading });
247
+ const setError = (newError: string | null) => updateActiveTab({ error: newError });
248
+
249
+ useEffect(() => {
250
+ let mounted = true;
251
+ const fetchConnectionConfig = async () => {
252
+ if (!databaseAdmin?.fetchAvailableDatabases || !databaseAdmin?.fetchAvailableRoles) {
253
+ setConnectionConfigError(t("studio_sql_sql_not_supported"));
254
+ setIsLoadingConfig(false);
255
+ return;
256
+ }
257
+
258
+ try {
259
+ const [dbs, roles, currentDbFromApi] = await Promise.all([
260
+ databaseAdmin.fetchAvailableDatabases(),
261
+ databaseAdmin.fetchAvailableRoles(),
262
+ typeof databaseAdmin?.fetchCurrentDatabase === "function" ? databaseAdmin.fetchCurrentDatabase() : Promise.resolve(undefined)
263
+ ]);
264
+
265
+ if (mounted) {
266
+ setAvailableDatabases(dbs);
267
+ setAvailableRoles(roles);
268
+
269
+ const loadedDb = localStorage.getItem("rebase_sql_selected_db") || undefined;
270
+ const loadedRole = localStorage.getItem("rebase_sql_selected_role") || undefined;
271
+
272
+ const initialActiveTabId = localStorage.getItem(STORAGE_KEY_ACTIVE_TAB) || "1";
273
+ let initialTabs: any[] = [];
274
+ try {
275
+ const savedTabs = localStorage.getItem(STORAGE_KEY_TABS);
276
+ if (savedTabs) initialTabs = JSON.parse(savedTabs);
277
+ } catch (e) { /* ignore */ }
278
+ const currentActiveTab = initialTabs.find(t => t.id === initialActiveTabId);
279
+
280
+ let actualDb = currentActiveTab?.database || loadedDb;
281
+ if (actualDb && !dbs.includes(actualDb)) actualDb = undefined;
282
+ if (!actualDb && dbs.length > 0) {
283
+ actualDb = currentDbFromApi && dbs.includes(currentDbFromApi) ? currentDbFromApi : dbs[0];
284
+ }
285
+
286
+ if (actualDb) {
287
+ setSelectedDatabase(actualDb);
288
+ localStorage.setItem("rebase_sql_selected_db", actualDb);
289
+ setTabs(prev => prev.map(t => t.id === initialActiveTabId && !t.database ? { ...t,
290
+ database: actualDb } : t));
291
+ }
292
+
293
+ let actualRole = currentActiveTab?.role || loadedRole;
294
+ if (actualRole && !roles.includes(actualRole)) actualRole = undefined;
295
+ if (!actualRole && roles.length > 0) {
296
+ actualRole = roles.includes("postgres") ? "postgres" : roles[0];
297
+ }
298
+
299
+ if (actualRole) {
300
+ setSelectedRole(actualRole);
301
+ localStorage.setItem("rebase_sql_selected_role", actualRole);
302
+ setTabs(prev => prev.map(t => t.id === initialActiveTabId && !t.role ? { ...t,
303
+ role: actualRole } : t));
304
+ }
305
+ }
306
+ } catch (err: unknown) {
307
+ console.error("Failed to fetch databases or roles:", err);
308
+ if (mounted) {
309
+ const message = err instanceof Error ? err.message : String(err);
310
+ setConnectionConfigError(t("studio_sql_fetch_error", { message }));
311
+ }
312
+ } finally {
313
+ if (mounted) {
314
+ setIsLoadingConfig(false);
315
+ }
316
+ }
317
+ };
318
+
319
+ fetchConnectionConfig();
320
+
321
+ return () => { mounted = false; };
322
+ }, [databaseAdmin]);
323
+
324
+ const handleDatabaseChange = (db: string, tabId?: string) => {
325
+ setSelectedDatabase(db);
326
+ localStorage.setItem("rebase_sql_selected_db", db);
327
+ setTabs(prev => prev.map(t => t.id === (tabId || activeTabId) ? { ...t,
328
+ database: db } : t));
329
+ // Reset so the schema will be re-fetched for the new database
330
+ schemaFetchedRef.current = false;
331
+ };
332
+
333
+ const handleRoleChange = (role: string, tabId?: string) => {
334
+ setSelectedRole(role);
335
+ localStorage.setItem("rebase_sql_selected_role", role);
336
+ setTabs(prev => prev.map(t => t.id === (tabId || activeTabId) ? { ...t,
337
+ role } : t));
338
+ };
339
+
340
+ const handleTabChange = useCallback((newTabId: string) => {
341
+ setActiveTabId(newTabId);
342
+ const newTab = tabs.find(t => t.id === newTabId);
343
+ if (newTab) {
344
+ if (newTab.database && newTab.database !== selectedDatabase) {
345
+ setSelectedDatabase(newTab.database);
346
+ localStorage.setItem("rebase_sql_selected_db", newTab.database);
347
+ schemaFetchedRef.current = false;
348
+ } else if (!newTab.database && selectedDatabase) {
349
+ setTabs(prev => prev.map(t => t.id === newTabId ? { ...t,
350
+ database: selectedDatabase } : t));
351
+ }
352
+
353
+ if (newTab.role && newTab.role !== selectedRole) {
354
+ setSelectedRole(newTab.role);
355
+ localStorage.setItem("rebase_sql_selected_role", newTab.role);
356
+ } else if (!newTab.role && selectedRole) {
357
+ setTabs(prev => prev.map(t => t.id === newTabId ? { ...t,
358
+ role: selectedRole } : t));
359
+ }
360
+ }
361
+ }, [tabs, selectedDatabase, selectedRole]);
362
+
363
+ const fetchSchema = useCallback(async () => {
364
+ if (!databaseAdmin?.executeSql) {
365
+ setSchemaError(t("studio_sql_sql_not_supported"));
366
+ setIsSchemaLoading(false);
367
+ return;
368
+ }
369
+
370
+ setIsSchemaLoading(true);
371
+ setSchemaError(null);
372
+ try {
373
+ const sql = `
374
+ SELECT
375
+ c.table_schema as schema,
376
+ c.table_name as "table",
377
+ c.column_name as "column",
378
+ c.data_type as "data_type",
379
+ CASE WHEN kcu.column_name IS NOT NULL THEN true ELSE false END as "is_pk"
380
+ FROM
381
+ information_schema.columns c
382
+ LEFT JOIN information_schema.table_constraints tc
383
+ ON tc.table_schema = c.table_schema
384
+ AND tc.table_name = c.table_name
385
+ AND tc.constraint_type = 'PRIMARY KEY'
386
+ LEFT JOIN information_schema.key_column_usage kcu
387
+ ON kcu.constraint_name = tc.constraint_name
388
+ AND kcu.table_schema = tc.table_schema
389
+ AND kcu.table_name = tc.table_name
390
+ AND kcu.column_name = c.column_name
391
+ WHERE
392
+ c.table_schema NOT IN ('information_schema', 'pg_catalog')
393
+ ORDER BY
394
+ c.table_schema, c.table_name, c.ordinal_position;
395
+ `;
396
+ // Pass the selected database so schema introspection targets the right DB.
397
+ const result = await databaseAdmin!.executeSql!(sql, { database: selectedDatabase });
398
+
399
+ const processGrouped = (data: Record<string, unknown>[]) => {
400
+ const grouped = data.reduce((acc: Record<string, TableInfo[]>, curr: Record<string, unknown>) => {
401
+ const schema = (curr.schema || curr.SCHEMA || curr.table_schema || "public") as string;
402
+ const table = (curr.table || curr.TABLE || curr.table_name) as string;
403
+ const column = (curr.column || curr.COLUMN || curr.column_name) as string;
404
+ const dataType = (curr.data_type || curr.DATA_TYPE || "") as string;
405
+ const isPrimaryKey = curr.is_pk === true || curr.is_pk === "true";
406
+
407
+ if (!acc[schema]) acc[schema] = [];
408
+ let tableInfo = acc[schema].find(t => t.tableName === table);
409
+ if (!tableInfo) {
410
+ tableInfo = { schemaName: schema,
411
+ tableName: table,
412
+ columns: [] };
413
+ acc[schema].push(tableInfo);
414
+ }
415
+ tableInfo.columns.push({ name: column,
416
+ dataType,
417
+ isPrimaryKey });
418
+ return acc;
419
+ }, {});
420
+ setSchemas(grouped);
421
+ };
422
+
423
+ if (!result || !Array.isArray(result)) {
424
+ if (result && typeof result === "object" && "rows" in result && Array.isArray((result as { rows: Record<string, unknown>[] }).rows)) {
425
+ processGrouped((result as { rows: Record<string, unknown>[] }).rows);
426
+ } else {
427
+ setSchemaError(t("studio_sql_unexpected_format", { type: typeof result }));
428
+ setSchemas({});
429
+ }
430
+ } else if (result.length === 0) {
431
+ setSchemas({});
432
+ setSchemaError(t("studio_sql_no_tables"));
433
+ } else {
434
+ processGrouped(result);
435
+ }
436
+
437
+ schemaFetchedRef.current = true;
438
+ } catch (e: unknown) {
439
+ console.error("Schema fetch error:", e);
440
+ const message = e instanceof Error ? e.message : String(e);
441
+ setSchemaError(t("studio_sql_schema_fetch_error", { message }));
442
+ } finally {
443
+ setIsSchemaLoading(false);
444
+ }
445
+ }, [databaseAdmin, selectedDatabase]);
446
+
447
+ useEffect(() => {
448
+ // Fetch schema after config finishes loading, and re-fetch when the selected database changes.
449
+ if (!isLoadingConfig && !schemaFetchedRef.current) {
450
+ fetchSchema();
451
+ }
452
+ }, [fetchSchema, isLoadingConfig, selectedDatabase]);
453
+
454
+ const [autoLimit, setAutoLimit] = useState(true);
455
+ const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false);
456
+ const [pendingAction, setPendingAction] = useState<(() => void) | null>(null);
457
+
458
+ // Inline editing state
459
+ const [editingCell, setEditingCell] = useState<{ rowIndex: number, columnKey: string, initialValue: unknown } | null>(null);
460
+
461
+ const handleDoubleClick = useCallback((rowIndex: number, columnKey: string, initialValue: unknown, rowData: Record<string, unknown>) => {
462
+ if (!activeTab.lastExecutedSql) {
463
+ snackbarController.open({
464
+ type: "error",
465
+ message: t("studio_sql_cannot_edit_missing_query")
466
+ });
467
+ return;
468
+ }
469
+
470
+ const resolution = determineTableAndPK(activeTab.lastExecutedSql, columnKey, schemas);
471
+
472
+ if (resolution.error || !resolution.primaryKeys || resolution.primaryKeys.length === 0) {
473
+ snackbarController.open({
474
+ type: "error",
475
+ message: resolution.error || t("studio_sql_cannot_resolve_table")
476
+ });
477
+ return;
478
+ }
479
+
480
+ // Check all PK values are present in the row
481
+ const missingPKs = resolution.primaryKeys.filter(
482
+ pk => rowData[pk.resultColumn] === undefined || rowData[pk.resultColumn] === null
483
+ );
484
+ if (missingPKs.length > 0) {
485
+ snackbarController.open({
486
+ type: "error",
487
+ message: t("studio_sql_missing_pk", { columns: missingPKs.map(pk => `"${pk.resultColumn}"`).join(", ") })
488
+ });
489
+ return;
490
+ }
491
+
492
+ setEditingCell({ rowIndex,
493
+ columnKey,
494
+ initialValue });
495
+ }, [activeTab.lastExecutedSql, schemas, snackbarController]);
496
+
497
+ const handleCellSave = useCallback(async (newValue: string | null, rowData: Record<string, unknown>, columnKey: string, rowIndex: number) => {
498
+ if (!editingCell || !activeTab.lastExecutedSql) return;
499
+
500
+ setEditingCell(null); // Optimistically close
501
+
502
+ if (newValue === editingCell.initialValue) return;
503
+
504
+ const resolution = determineTableAndPK(activeTab.lastExecutedSql, columnKey, schemas);
505
+ if (resolution.error || !resolution.tableName || !resolution.primaryKeys || resolution.primaryKeys.length === 0) {
506
+ snackbarController.open({ type: "error",
507
+ message: resolution.error || "Resolution failed." });
508
+ return;
509
+ }
510
+
511
+ const tableName = resolution.tableName;
512
+
513
+ const formatValue = (val: unknown) => {
514
+ if (val === null || val === undefined) return "NULL";
515
+ if (typeof val === "number") return val;
516
+ if (typeof val === "boolean") return val ? "TRUE" : "FALSE";
517
+ return `'${String(val).replace(/'/g, "''")}'`;
518
+ };
519
+
520
+ // Resolve the actual DB column name for the edited column (may differ from the result alias)
521
+ // e.g. if the query has `a.name AS author_name`, columnKey = "author_name" but DB column = "name"
522
+ const resolveDbColumnName = (resultColKey: string): string => {
523
+ try {
524
+ const ast = parseFirst(activeTab.lastExecutedSql!);
525
+ if (ast.type === "select" && ast.columns) {
526
+ for (const col of ast.columns) {
527
+ if (col.expr?.type === "ref") {
528
+ const alias = col.alias?.name;
529
+ const colName = col.expr.name;
530
+ if (alias === resultColKey || (!alias && colName === resultColKey)) {
531
+ return colName;
532
+ }
533
+ }
534
+ }
535
+ }
536
+ } catch { /* fall back to columnKey */ }
537
+ return resultColKey;
538
+ };
539
+
540
+ const dbColumnName = resolveDbColumnName(columnKey);
541
+
542
+ // Build composite WHERE clause
543
+ const whereConditions = resolution.primaryKeys.map(
544
+ pk => `"${pk.dbColumn}" = ${formatValue(rowData[pk.resultColumn])}`
545
+ ).join(" AND ");
546
+
547
+ const updateSql = `UPDATE "${tableName}" SET "${dbColumnName}" = ${formatValue(newValue)} WHERE ${whereConditions};`;
548
+
549
+ try {
550
+ if (databaseAdmin?.executeSql) {
551
+ await databaseAdmin.executeSql(updateSql, { database: selectedDatabase,
552
+ role: selectedRole });
553
+
554
+ const newResults = [...(activeTab.results || [])];
555
+ if (newResults[rowIndex]) {
556
+ newResults[rowIndex] = { ...newResults[rowIndex],
557
+ [columnKey]: newValue };
558
+ }
559
+ updateActiveTab({ results: newResults });
560
+
561
+ snackbarController.open({
562
+ type: "success",
563
+ message: t("studio_sql_row_updated")
564
+ });
565
+ }
566
+ } catch (e: unknown) {
567
+ snackbarController.open({
568
+ type: "error",
569
+ message: t("studio_sql_update_failed", { message: e instanceof Error ? e.message : String(e) })
570
+ });
571
+ }
572
+ }, [editingCell, schemas, activeTab.lastExecutedSql, activeTab.results, databaseAdmin, updateActiveTab, snackbarController, selectedDatabase, selectedRole]);
573
+
574
+ const [columnWidths, setColumnWidths] = useState<Record<string, Record<string, number>>>(() => {
575
+ const saved = localStorage.getItem("rebase_sql_column_widths");
576
+ return saved ? JSON.parse(saved) : {};
577
+ });
578
+ const [snippets, setSnippets] = useState<Snippet[]>([]);
579
+ const [history, setHistory] = useState<string[]>([]);
580
+ const [isSaveDialogOpen, setIsSaveDialogOpen] = useState(false);
581
+ const [newSnippetName, setNewSnippetName] = useState("");
582
+
583
+ // Load from local storage
584
+ useEffect(() => {
585
+ const savedSnippets = localStorage.getItem("rebase_sql_snippets");
586
+ if (savedSnippets) setSnippets(JSON.parse(savedSnippets));
587
+
588
+ const savedHistory = localStorage.getItem("rebase_sql_history");
589
+ if (savedHistory) setHistory(JSON.parse(savedHistory));
590
+ }, []);
591
+
592
+ // Save tabs and active tab to local storage
593
+ useEffect(() => {
594
+ const sanitizedTabs = tabs.map(t => ({
595
+ id: t.id,
596
+ name: t.name,
597
+ sql: t.sql,
598
+ database: t.database,
599
+ role: t.role
600
+ }));
601
+ localStorage.setItem(STORAGE_KEY_TABS, JSON.stringify(sanitizedTabs));
602
+ }, [tabs]);
603
+
604
+ useEffect(() => {
605
+ localStorage.setItem(STORAGE_KEY_ACTIVE_TAB, activeTabId);
606
+ }, [activeTabId]);
607
+
608
+ const saveSnippets = (newSnippets: Snippet[]) => {
609
+ setSnippets(newSnippets);
610
+ localStorage.setItem("rebase_sql_snippets", JSON.stringify(newSnippets));
611
+ };
612
+
613
+ const saveHistory = (newHistory: string[]) => {
614
+ setHistory(newHistory);
615
+ localStorage.setItem("rebase_sql_history", JSON.stringify(newHistory.slice(-50)));
616
+ };
617
+
618
+ const handleDeleteSnippet = (id: string) => {
619
+ saveSnippets(snippets.filter(s => s.id !== id));
620
+ };
621
+
622
+ const handleAddTab = () => {
623
+ const newId = Math.random().toString(36).substring(2, 9);
624
+
625
+ // Find the next available query number
626
+ let maxNumber = 0;
627
+ tabs.forEach(tab => {
628
+ const match = tab.name.match(/^Query (\d+)$/);
629
+ if (match) {
630
+ const num = parseInt(match[1], 10);
631
+ if (num > maxNumber) maxNumber = num;
632
+ }
633
+ });
634
+ const name = `Query ${maxNumber + 1}`;
635
+ setTabs(prev => [...prev, {
636
+ id: newId,
637
+ name,
638
+ sql: "SELECT * FROM ",
639
+ database: selectedDatabase,
640
+ role: selectedRole,
641
+ results: null,
642
+ loading: false,
643
+ error: null,
644
+ execTime: null,
645
+ lastExecutedSql: null
646
+ }]);
647
+ setActiveTabId(newId);
648
+ };
649
+
650
+ const handleCloseTab = (id: string, e: React.MouseEvent) => {
651
+ e.stopPropagation();
652
+ if (tabs.length === 1) return;
653
+
654
+ const tabIndex = tabs.findIndex(t => t.id === id);
655
+ const newTabs = tabs.filter(t => t.id !== id);
656
+ setTabs(newTabs);
657
+
658
+ if (activeTabId === id) {
659
+ // Find a new active tab: the one at the same index, or the last one if we closed the last
660
+ const nextIndex = Math.min(tabIndex, newTabs.length - 1);
661
+ if (newTabs[nextIndex]) {
662
+ setActiveTabId(newTabs[nextIndex].id);
663
+ }
664
+ }
665
+ };
666
+
667
+ const handleColumnResize = useCallback(({ key, width }: { key: string, width: number }) => {
668
+ setColumnWidths(prev => {
669
+ const newWidths = {
670
+ ...prev,
671
+ [activeTab.sql]: {
672
+ ...(prev[activeTab.sql] || {}),
673
+ [key]: width
674
+ }
675
+ };
676
+ localStorage.setItem("rebase_sql_column_widths", JSON.stringify(newWidths));
677
+ return newWidths;
678
+ });
679
+ }, [activeTab.sql]);
680
+
681
+ const handlePrettify = () => {
682
+ // Simple formatting for now
683
+ const formatted = activeTab.sql
684
+ .replace(/\s+/g, " ")
685
+ .replace(/\s?,\s?/g, ", ")
686
+ .replace(/\s?=\s?/g, " = ")
687
+ .trim();
688
+ setSql(formatted);
689
+ };
690
+
691
+ const handleExplain = async () => {
692
+ const explainSql = `EXPLAIN (FORMAT JSON, ANALYZE) ${activeTab.sql}`;
693
+ updateActiveTab({ loading: true,
694
+ error: null,
695
+ results: null });
696
+ const start = performance.now();
697
+ try {
698
+ if (databaseAdmin?.executeSql) {
699
+ const result = await databaseAdmin.executeSql(explainSql, { database: selectedDatabase,
700
+ role: selectedRole });
701
+ updateActiveTab({ results: result,
702
+ execTime: Math.round(performance.now() - start) });
703
+ }
704
+ } catch (e: unknown) {
705
+ const message = e instanceof Error ? e.message : String(e);
706
+ updateActiveTab({ error: message || t("studio_sql_error_explaining") });
707
+ } finally {
708
+ updateActiveTab({ loading: false });
709
+ }
710
+ };
711
+
712
+ const executeRun = useCallback(async (sqlOverride?: string) => {
713
+ let sqlToRun = sqlOverride || activeTab.sql;
714
+ const upperSql = sqlToRun.toUpperCase();
715
+
716
+ const isAggregate = /\b(COUNT|SUM|AVG|MIN|MAX)\s*\(/i.test(sqlToRun);
717
+ const isExplain = /\bEXPLAIN\b/i.test(sqlToRun);
718
+
719
+ if (autoLimit && upperSql.includes("SELECT") && !upperSql.includes("LIMIT") && !isAggregate && !isExplain) {
720
+ // Remove trailing semicolon if present to safely append LIMIT
721
+ sqlToRun = sqlToRun.trim().replace(/;$/, "");
722
+ sqlToRun = `${sqlToRun} LIMIT 1000;`;
723
+ }
724
+
725
+ updateActiveTab({ loading: true,
726
+ error: null,
727
+ results: null });
728
+ const start = performance.now();
729
+
730
+ try {
731
+ if (databaseAdmin?.executeSql) {
732
+ const result = await databaseAdmin.executeSql(sqlToRun, { database: selectedDatabase,
733
+ role: selectedRole });
734
+ updateActiveTab({
735
+ results: result,
736
+ execTime: Math.round(performance.now() - start),
737
+ lastExecutedSql: sqlToRun
738
+ });
739
+
740
+ if (history[history.length - 1] !== activeTab.sql) {
741
+ saveHistory([...history, activeTab.sql]);
742
+ }
743
+ } else {
744
+ updateActiveTab({ error: t("studio_sql_execution_not_supported") });
745
+ }
746
+ } catch (e: unknown) {
747
+ const message = e instanceof Error ? e.message : String(e);
748
+ updateActiveTab({ error: message || t("studio_sql_error_executing") });
749
+ } finally {
750
+ updateActiveTab({ loading: false });
751
+ }
752
+ }, [activeTab.sql, autoLimit, databaseAdmin, history, updateActiveTab]);
753
+
754
+ const handleRun = useCallback(async (selectedText?: string) => {
755
+ const sqlTarget = selectedText || activeTab.sql;
756
+ if (!sqlTarget.trim()) return;
757
+
758
+ // Destructive operation check
759
+ const destructiveKeywords = ["DELETE", "DROP", "TRUNCATE", "UPDATE"];
760
+ const hasDestructive = destructiveKeywords.some(kw => sqlTarget.toUpperCase().includes(kw));
761
+ const hasWhere = sqlTarget.toUpperCase().includes("WHERE");
762
+
763
+ if (hasDestructive && (!hasWhere || sqlTarget.toUpperCase().includes("DROP") || sqlTarget.toUpperCase().includes("TRUNCATE"))) {
764
+ setPendingAction(() => () => executeRun(selectedText));
765
+ setIsConfirmDialogOpen(true);
766
+ return;
767
+ }
768
+
769
+ executeRun(selectedText);
770
+ }, [activeTab.sql, executeRun]);
771
+
772
+ // Global keybindings
773
+ useEffect(() => {
774
+ const handleKeyDown = (e: KeyboardEvent) => {
775
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
776
+ // If we are in an input or textarea (except the code editor which handles its own), we might not want to run
777
+ const activeElement = document.activeElement;
778
+ const isInput = activeElement?.tagName === "INPUT" || activeElement?.tagName === "TEXTAREA";
779
+ // If it's the monaco editor textarea, it's fine, let's trigger handleRun
780
+ // Actually the monaco editor already has its own action, so we don't need a global one IF focused in monaco.
781
+ // But wait, if we have both, it might run twice.
782
+ // Let's check if we're focused in monaco.
783
+ const isMonaco = activeElement?.className?.includes("monaco-mouse-cursor-text");
784
+
785
+ if (!isMonaco && !isInput) {
786
+ e.preventDefault();
787
+ handleRun();
788
+ }
789
+ }
790
+ };
791
+ window.addEventListener("keydown", handleKeyDown);
792
+ return () => window.removeEventListener("keydown", handleKeyDown);
793
+ }, [handleRun]);
794
+
795
+ const handleSaveSnippet = () => {
796
+ if (!newSnippetName.trim() || !sql.trim()) return;
797
+
798
+ const newSnippet: Snippet = {
799
+ id: Math.random().toString(36).substring(2, 9),
800
+ name: newSnippetName,
801
+ sql: sql,
802
+ createdAt: Date.now()
803
+ };
804
+
805
+ saveSnippets([...snippets, newSnippet]);
806
+ setNewSnippetName("");
807
+ setIsSaveDialogOpen(false);
808
+ snackbarController.open({
809
+ type: "success",
810
+ message: t("studio_sql_snippet_saved", { name: newSnippetName })
811
+ });
812
+ };
813
+
814
+ const handleExportCSV = () => {
815
+ if (!results || results.length === 0) return;
816
+
817
+ const headers = Object.keys(results[0]).join(",");
818
+ const rows = results.map(row =>
819
+ Object.values(row).map(val => {
820
+ const str = String(val);
821
+ return str.includes(",") ? `"${str}"` : str;
822
+ }).join(",")
823
+ );
824
+ const csv = [headers, ...rows].join("\n");
825
+ const blob = new Blob([csv], { type: "text/csv" });
826
+ const url = window.URL.createObjectURL(blob);
827
+ const a = document.createElement("a");
828
+ a.href = url;
829
+ a.download = `query_results_${new Date().toISOString().slice(0, 19)}.csv`;
830
+ a.click();
831
+ window.URL.revokeObjectURL(url);
832
+ };
833
+
834
+ const handleExportJSON = () => {
835
+ if (!results || results.length === 0) return;
836
+
837
+ const json = JSON.stringify(results, null, 2);
838
+ const blob = new Blob([json], { type: "application/json" });
839
+ const url = window.URL.createObjectURL(blob);
840
+ const a = document.createElement("a");
841
+ a.href = url;
842
+ a.download = `query_results_${new Date().toISOString().slice(0, 19)}.json`;
843
+ a.click();
844
+ window.URL.revokeObjectURL(url);
845
+ };
846
+
847
+ const handleExportMarkdown = () => {
848
+ if (!results || results.length === 0) return;
849
+
850
+ const headers = Object.keys(results[0]);
851
+ const headerRow = `| ${headers.join(" | ")} |`;
852
+ const dividerRow = `| ${headers.map(() => "---").join(" | ")} |`;
853
+ const dataRows = results.map(row =>
854
+ `| ${headers.map(header => {
855
+ const val = row[header];
856
+ if (val === null) return "null";
857
+ if (val === undefined) return "";
858
+ // Replace pipes and newlines to avoid breaking the markdown table
859
+ return String(val).replace(/\|/g, "\\|").replace(/\n/g, " ");
860
+ }).join(" | ")} |`
861
+ );
862
+
863
+ const markdown = [headerRow, dividerRow, ...dataRows].join("\n");
864
+ navigator.clipboard.writeText(markdown).then(() => {
865
+ snackbarController.open({
866
+ type: "success",
867
+ message: t("studio_sql_markdown_copied")
868
+ });
869
+ }).catch(() => {
870
+ snackbarController.open({
871
+ type: "error",
872
+ message: t("studio_sql_markdown_copy_failed")
873
+ });
874
+ });
875
+ };
876
+
877
+ const renderResults = () => {
878
+ if (loading) {
879
+ return (
880
+ <div className="flex-grow flex items-center justify-center">
881
+ <div className="text-center">
882
+ <CircularProgress size="medium"/>
883
+ <Typography variant="body2" className="mt-4 text-text-secondary dark:text-text-secondary-dark font-mono tracking-tight animate-pulse">{t("studio_sql_executing_query")}</Typography>
884
+ </div>
885
+ </div>
886
+ );
887
+ }
888
+
889
+ if (error) {
890
+ return (
891
+ <div className="flex-grow flex items-center justify-center p-6 overflow-auto">
892
+ <ErrorView title={t("studio_sql_query_error")} error={error}/>
893
+ </div>
894
+ );
895
+ }
896
+
897
+ if (!results) {
898
+ return (
899
+ <div className="flex-grow flex items-center justify-center text-text-disabled dark:text-text-disabled-dark">
900
+ <div className="text-center">
901
+ <svg className="w-12 h-12 mx-auto mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1} d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"/></svg>
902
+ <Typography variant="body2">{t("studio_sql_run_query_placeholder")}</Typography>
903
+ </div>
904
+ </div>
905
+ );
906
+ }
907
+
908
+ // Check for EXPLAIN (FORMAT JSON) response
909
+ if (results.length === 1 && results[0]["QUERY PLAN"] && Array.isArray(results[0]["QUERY PLAN"])) {
910
+ try {
911
+ const plan = results[0]["QUERY PLAN"][0].Plan;
912
+ if (plan) {
913
+ return (
914
+ <div className="flex-grow overflow-auto p-4 bg-surface-50 dark:bg-surface-900 flex flex-col items-start">
915
+ <Typography variant="caption" className="font-bold text-text-secondary mb-4 tracking-wider uppercase">{t("studio_sql_visual_execution_plan")}</Typography>
916
+ <div className="pb-12">
917
+ <ExplainVisualizer plan={plan}/>
918
+ </div>
919
+ </div>
920
+ );
921
+ }
922
+ } catch (e) {
923
+ console.warn("Failed to parse EXPLAIN JSON output:", e);
924
+ }
925
+ }
926
+
927
+ if (results.length === 0) {
928
+ return (
929
+ <div className="flex-grow p-6 flex flex-col items-center justify-center">
930
+ <Typography variant="body2" className="text-text-secondary dark:text-text-secondary-dark font-mono border-b border-surface-200 dark:border-surface-950 pb-2 mb-2">{t("studio_sql_success")}</Typography>
931
+ <Typography variant="caption" className="text-text-disabled dark:text-text-disabled-dark">{t("studio_sql_no_results")}</Typography>
932
+ </div>
933
+ );
934
+ }
935
+
936
+ const savedWidths = columnWidths[activeTab.sql] || {};
937
+ const resultColumnKeys = Object.keys(results[0]);
938
+
939
+ // Compute matched collections for this query, including PK column detection
940
+ const matchedCollections: ResolvedQueryCollection[] = (() => {
941
+ if (!activeTab.lastExecutedSql || !collectionRegistry.collections) return [];
942
+ try {
943
+ return resolveQueryCollections(activeTab.lastExecutedSql, schemas, collectionRegistry.collections, resultColumnKeys);
944
+ } catch {
945
+ return [];
946
+ }
947
+ })();
948
+
949
+ // Only collections that have a PK column in the result set can be opened
950
+ const actionableCollections = matchedCollections.filter(mc => mc.pkColumn && resultColumnKeys.includes(mc.pkColumn));
951
+
952
+ // For each row, determine which entities can be opened
953
+ const getRowEntityActions = (rowData: Record<string, unknown>): { collection: ResolvedQueryCollection, entityId: string | number }[] => {
954
+ if (!rowData) return [];
955
+ return actionableCollections
956
+ .filter(mc => rowData[mc.pkColumn!] != null)
957
+ .map(mc => ({
958
+ collection: mc,
959
+ entityId: rowData[mc.pkColumn!] as string | number
960
+ }));
961
+ };
962
+
963
+ // Build the columns array. If we have actionable collections, prepend a dedicated action column.
964
+ const dataColumns: VirtualTableColumn[] = resultColumnKeys.map(key => ({
965
+ key,
966
+ title: key,
967
+ width: savedWidths[key] ?? 150,
968
+ sortable: false,
969
+ resizable: true
970
+ }));
971
+
972
+ const columns: VirtualTableColumn[] = actionableCollections.length > 0
973
+ ? [{ key: "__cms_action__",
974
+ title: "",
975
+ width: 36,
976
+ sortable: false,
977
+ resizable: false }, ...dataColumns]
978
+ : dataColumns;
979
+
980
+ return (
981
+ <div className="flex-grow flex flex-col overflow-hidden min-h-0">
982
+ {/* Collection Badges Bar */}
983
+ {actionableCollections.length > 0 && (
984
+ <div className={cls("px-4 py-1.5 border-b flex items-center gap-2 shrink-0 bg-surface-50 dark:bg-surface-900", defaultBorderMixin)}>
985
+ <Tooltip title={t("studio_sql_cms_collections_tooltip")}>
986
+ <Typography variant="caption" className="text-[10px] font-bold uppercase tracking-widest text-text-disabled dark:text-text-disabled-dark mr-1 shrink-0 cursor-help">{t("studio_sql_cms")}</Typography>
987
+ </Tooltip>
988
+ <div className="flex items-center gap-1.5 overflow-x-auto no-scrollbar">
989
+ {actionableCollections.map(mc => (
990
+ <Tooltip key={mc.tableName} title={`Table "${mc.tableName}" → ${mc.collection.name} (PK: ${mc.pkColumn})`}>
991
+ <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-medium bg-primary/10 dark:bg-primary-dark/15 text-primary dark:text-primary-dark whitespace-nowrap border border-primary/20 dark:border-primary-dark/20">
992
+ {typeof mc.collection.icon === "string" && (
993
+ <IconForView collectionOrView={mc.collection} className="text-[12px]"/>
994
+ )}
995
+ {mc.collection.name}
996
+ </span>
997
+ </Tooltip>
998
+ ))}
999
+ </div>
1000
+ </div>
1001
+ )}
1002
+ <div className="flex-grow relative h-full min-h-0 min-w-0">
1003
+ <VirtualTable
1004
+ data={results}
1005
+ columns={columns}
1006
+ rowHeight={32}
1007
+ headerHeight={32}
1008
+ extraData={editingCell}
1009
+ onColumnResizeEnd={handleColumnResize}
1010
+ cellRenderer={({ rowData, column, rowIndex }) => {
1011
+ // Dedicated collection action column
1012
+ if (column.key === "__cms_action__") {
1013
+ const rowActions = getRowEntityActions(rowData);
1014
+ if (rowActions.length === 0) {
1015
+ return <div className="h-full w-full"/>;
1016
+ }
1017
+ if (rowActions.length === 1) {
1018
+ const ra = rowActions[0];
1019
+ return (
1020
+ <div className="h-full flex items-center justify-center">
1021
+ <Tooltip title={t("studio_sql_edit_entity", { name: ra.collection.collection.name,
1022
+ id: String(ra.entityId) })}>
1023
+ <IconButton
1024
+ size="small"
1025
+ className="text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-300 transition-colors"
1026
+ onClick={(e) => {
1027
+ e.stopPropagation();
1028
+ sideEntityController?.open({
1029
+ path: ra.collection.collection.slug,
1030
+ entityId: ra.entityId,
1031
+ collection: ra.collection.collection,
1032
+ updateUrl: false
1033
+ });
1034
+ }}
1035
+ >
1036
+ <PencilIcon size={iconSize.smallest}/>
1037
+ </IconButton>
1038
+ </Tooltip>
1039
+ </div>
1040
+ );
1041
+ }
1042
+ // Multiple matched collections (JOIN) — show a dropdown
1043
+ return (
1044
+ <div className="h-full flex items-center justify-center">
1045
+ <Menu
1046
+ trigger={
1047
+ <IconButton
1048
+ size="small"
1049
+ className="text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-300 transition-colors"
1050
+ onClick={(e) => e.stopPropagation()}
1051
+ >
1052
+ <MoreVerticalIcon size={iconSize.smallest}/>
1053
+ </IconButton>
1054
+ }
1055
+ >
1056
+ {rowActions.map(ra => (
1057
+ <MenuItem
1058
+ key={ra.collection.tableName}
1059
+ dense
1060
+ onClick={() => {
1061
+ sideEntityController?.open({
1062
+ path: ra.collection.collection.slug,
1063
+ entityId: ra.entityId,
1064
+ collection: ra.collection.collection,
1065
+ updateUrl: false
1066
+ });
1067
+ }}
1068
+ >
1069
+ {t("studio_sql_edit_entity", { name: ra.collection.collection.name,
1070
+ id: String(ra.entityId) })}
1071
+ </MenuItem>
1072
+ ))}
1073
+ </Menu>
1074
+ </div>
1075
+ );
1076
+ }
1077
+
1078
+ // Regular data cell
1079
+ const isEditing = editingCell?.rowIndex === rowIndex && editingCell?.columnKey === column.key;
1080
+ const value = rowData ? rowData[column.key] : null;
1081
+ const displayValue = typeof value === "object" && value !== null ? JSON.stringify(value) : String(value ?? "");
1082
+
1083
+ if (isEditing) {
1084
+ return (
1085
+ <FixedEditorOverlay
1086
+ displayValue={displayValue}
1087
+ onSave={(val) => handleCellSave(val, rowData, column.key, rowIndex)}
1088
+ onCancel={() => setEditingCell(null)}
1089
+ />
1090
+ );
1091
+ }
1092
+
1093
+ return (
1094
+ <div
1095
+ className="px-4 py-1.5 h-full flex items-center whitespace-nowrap text-[13px] text-text-primary dark:text-text-primary-dark font-mono cursor-text group/cell"
1096
+ onDoubleClick={() => handleDoubleClick(rowIndex, column.key, displayValue, rowData)}
1097
+ >
1098
+ <div className="truncate flex-grow" title={displayValue}>
1099
+ {displayValue === "" ? <span className="text-text-disabled dark:text-text-disabled-dark italic text-[11px]">NULL</span> : displayValue}
1100
+ </div>
1101
+ </div>
1102
+ );
1103
+ }}
1104
+ />
1105
+ </div>
1106
+
1107
+ <div className={cls("p-2 px-4 border-t bg-surface-50 dark:bg-surface-900 flex justify-between items-center shrink-0", defaultBorderMixin)}>
1108
+ <div className="flex space-x-4">
1109
+ <div className="flex items-center text-[11px]">
1110
+ <span className="font-bold text-text-disabled dark:text-text-disabled-dark mr-2 uppercase tracking-tighter">{t("studio_sql_rows")}</span>
1111
+ <span className="font-mono text-text-secondary dark:text-text-secondary-dark">{results.length}</span>
1112
+ </div>
1113
+ <div className="flex items-center text-[11px]">
1114
+ <span className="font-bold text-text-disabled dark:text-text-disabled-dark mr-2 uppercase tracking-tighter">{t("studio_sql_time")}</span>
1115
+ <span className="font-mono text-text-secondary dark:text-text-secondary-dark">{execTime}ms</span>
1116
+ </div>
1117
+ </div>
1118
+ <div className="flex gap-2 overflow-x-auto no-scrollbar items-center px-2">
1119
+ <Button
1120
+ size="small"
1121
+ variant="text"
1122
+ className="text-[10px] uppercase font-bold text-text-secondary dark:text-text-secondary-dark whitespace-nowrap"
1123
+ onClick={handleExportMarkdown}
1124
+ >
1125
+ {t("studio_sql_copy_markdown")}
1126
+ </Button>
1127
+ <Button
1128
+ size="small"
1129
+ variant="text"
1130
+ className="text-[10px] uppercase font-bold text-text-secondary dark:text-text-secondary-dark whitespace-nowrap"
1131
+ onClick={handleExportJSON}
1132
+ >
1133
+ {t("studio_sql_export_json")}
1134
+ </Button>
1135
+ <Button
1136
+ size="small"
1137
+ variant="text"
1138
+ className="text-[10px] uppercase font-bold text-text-secondary dark:text-text-secondary-dark whitespace-nowrap"
1139
+ onClick={handleExportCSV}
1140
+ >
1141
+ {t("studio_sql_export_csv")}
1142
+ </Button>
1143
+ </div>
1144
+ </div>
1145
+ </div>
1146
+ );
1147
+ };
1148
+
1149
+ const [sidebarSize, setSidebarSize] = useState(() => {
1150
+ try {
1151
+ const saved = localStorage.getItem("rebase_sql_editor_sidebar_size");
1152
+ return saved !== null ? parseFloat(saved) : 20;
1153
+ } catch (e) {
1154
+ return 20;
1155
+ }
1156
+ });
1157
+ const [editorHeight, setEditorHeight] = useState(() => {
1158
+ try {
1159
+ const saved = localStorage.getItem("rebase_sql_editor_height");
1160
+ return saved !== null ? parseFloat(saved) : 50;
1161
+ } catch (e) {
1162
+ return 50;
1163
+ }
1164
+ });
1165
+
1166
+ useEffect(() => {
1167
+ try {
1168
+ localStorage.setItem("rebase_sql_editor_sidebar_size", sidebarSize.toString());
1169
+ } catch (e) { /* ignore */ }
1170
+ }, [sidebarSize]);
1171
+
1172
+ useEffect(() => {
1173
+ try {
1174
+ localStorage.setItem("rebase_sql_editor_height", editorHeight.toString());
1175
+ } catch (e) { /* ignore */ }
1176
+ }, [editorHeight]);
1177
+
1178
+ const activeSnippet = snippets.find(s => s.sql === activeTab.sql);
1179
+ const isFavorite = activeSnippet?.isFavorite || false;
1180
+
1181
+ return (
1182
+ <div className="flex h-full w-full bg-white dark:bg-surface-950 overflow-hidden text-text-primary dark:text-text-primary-dark">
1183
+ <ResizablePanels
1184
+ orientation="horizontal"
1185
+ panelSizePercent={sidebarSize}
1186
+ onPanelSizeChange={setSidebarSize}
1187
+ minPanelSizePx={220}
1188
+ firstPanel={
1189
+ <SQLEditorSidebar
1190
+ snippets={snippets}
1191
+ history={history}
1192
+ onSelectSnippet={setSql}
1193
+ onTableClick={setSql}
1194
+ onDeleteSnippet={handleDeleteSnippet}
1195
+ schemas={schemas}
1196
+ isSchemaLoading={isSchemaLoading}
1197
+ schemaError={schemaError}
1198
+ onRetrySchema={fetchSchema}
1199
+ />
1200
+ }
1201
+ secondPanel={
1202
+ <div className="flex-grow flex flex-col min-w-0 h-full w-full">
1203
+ {/* Toolbar */}
1204
+ <div className={cls("flex items-center justify-between pr-2 border-b bg-white dark:bg-surface-950", defaultBorderMixin)}>
1205
+ <div className="flex items-center flex-grow overflow-hidden mr-4">
1206
+ <div className="flex items-center no-scrollbar overflow-x-auto min-w-0">
1207
+ <Tabs value={activeTabId} onValueChange={handleTabChange} variant="boxy" className="w-[unset] flex-shrink-0" innerClassName="bg-white dark:bg-surface-950">
1208
+ {tabs.map(tab => (
1209
+ <Tab key={tab.id} value={tab.id} className="flex items-center justify-between group max-w-[200px]">
1210
+ <TerminalIcon size={iconSize.smallest} className="text-blue-500 mr-1.5 flex-shrink-0"/>
1211
+ <span className="truncate">{tab.name}</span>
1212
+ {tabs.length > 1 && (
1213
+ <IconButton
1214
+ size="smallest"
1215
+ onClick={(e) => handleCloseTab(tab.id, e)}
1216
+ className="ml-1 !p-0.5 opacity-0 group-hover:opacity-100 hover:text-red-500 transition-opacity"
1217
+ >
1218
+ <XIcon size={iconSize.smallest}/>
1219
+ </IconButton>
1220
+ )}
1221
+ </Tab>
1222
+ ))}
1223
+ </Tabs>
1224
+ <IconButton
1225
+ size="small"
1226
+ onClick={handleAddTab}
1227
+ className="ml-2 flex-shrink-0"
1228
+ >
1229
+ <PlusIcon size={iconSize.smallest}/>
1230
+ </IconButton>
1231
+ </div>
1232
+ </div>
1233
+ <div className="flex shrink-0 items-center justify-end pr-2 gap-1.5">
1234
+ <Tooltip title={t("studio_sql_format_sql")}>
1235
+ <IconButton size="small" onClick={handlePrettify} className="text-text-secondary hover:text-text-primary transition-colors">
1236
+ <MenuIcon size={iconSize.smallest}/>
1237
+ </IconButton>
1238
+ </Tooltip>
1239
+
1240
+ <Button
1241
+ variant="text"
1242
+ size="small"
1243
+ onClick={handleExplain}
1244
+ disabled={loading}
1245
+ >
1246
+ {t("studio_sql_explain")}
1247
+ </Button>
1248
+
1249
+ <div className="h-4 w-px bg-surface-200 dark:bg-surface-950 mx-1"></div>
1250
+
1251
+ <div className="flex items-center space-x-2 px-2 cursor-pointer" onClick={() => setAutoLimit(!autoLimit)}>
1252
+ <Typography variant="caption" className="text-[11px] text-text-secondary cursor-pointer select-none">{t("studio_sql_limit_1000")}</Typography>
1253
+ <input
1254
+ type="checkbox"
1255
+ checked={autoLimit}
1256
+ onChange={(e) => setAutoLimit(e.target.checked)}
1257
+ onClick={(e) => e.stopPropagation()}
1258
+ className="w-3.5 h-3.5 rounded border-surface-300 text-primary focus:ring-primary cursor-pointer"
1259
+ />
1260
+ </div>
1261
+
1262
+ <div className="h-4 w-px bg-surface-200 dark:bg-surface-950 mx-1"></div>
1263
+
1264
+ <Tooltip title={isFavorite ? t("studio_sql_remove_from_favorites") : t("studio_sql_add_to_favorites")}>
1265
+ <IconButton
1266
+ size="small"
1267
+ onClick={() => {
1268
+ if (!activeSnippet) {
1269
+ snackbarController.open({
1270
+ type: "info",
1271
+ message: t("studio_sql_save_first_to_favorite")
1272
+ });
1273
+ return;
1274
+ }
1275
+ saveSnippets(snippets.map(s => s.id === activeSnippet.id ? { ...s,
1276
+ isFavorite: !s.isFavorite } : s));
1277
+ }}
1278
+ >
1279
+ <svg className={`w-4 h-4 ${isFavorite ? "text-red-500 fill-current" : "text-text-disabled dark:text-text-disabled-dark hover:text-text-primary"}`} fill={isFavorite ? "currentColor" : "none"} stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"/></svg>
1280
+ </IconButton>
1281
+ </Tooltip>
1282
+
1283
+ <Button
1284
+ variant="text"
1285
+ size="small"
1286
+ onClick={() => setIsSaveDialogOpen(true)}
1287
+ >
1288
+ {t("studio_sql_save")}
1289
+ </Button>
1290
+
1291
+ <div className="h-4 w-px bg-surface-200 dark:bg-surface-950 mx-1"></div>
1292
+
1293
+ <Menu
1294
+ trigger={
1295
+ <Button
1296
+ size="small"
1297
+ variant="outlined"
1298
+ className="text-text-secondary dark:text-text-secondary-dark font-medium mr-2"
1299
+ >
1300
+ <DatabaseIcon size={iconSize.small} className="mr-1.5 text-text-disabled dark:text-text-disabled-dark"/>
1301
+ <span className="max-w-[80px] truncate">{isLoadingConfig ? "..." : (selectedDatabase || t("studio_sql_select_db"))}</span>
1302
+ </Button>
1303
+ }
1304
+ >
1305
+ <div className="max-h-64 overflow-y-auto">
1306
+ <div className="px-3 py-1.5 border-b border-surface-200 dark:border-surface-950 mb-1">
1307
+ <Typography variant="caption" className="font-bold uppercase tracking-wider text-[9px] text-text-disabled dark:text-text-disabled-dark">{t("studio_sql_database")}</Typography>
1308
+ </div>
1309
+ {isLoadingConfig ? (
1310
+ <div className="flex items-center justify-center p-4">
1311
+ <CircularProgress size="small"/>
1312
+ </div>
1313
+ ) : connectionConfigError ? (
1314
+ <div className="px-3 py-2 text-xs text-red-500 dark:text-red-400 max-w-[200px] break-words">
1315
+ {connectionConfigError}
1316
+ </div>
1317
+ ) : (
1318
+ <>
1319
+ {availableDatabases.map(db => (
1320
+ <MenuItem key={db} dense onClick={() => handleDatabaseChange(db)} className={cls("text-xs", selectedDatabase === db && "text-primary dark:text-primary-dark")}>
1321
+ {db}
1322
+ </MenuItem>
1323
+ ))}
1324
+
1325
+ <div className="px-3 py-1.5 border-y border-surface-200 dark:border-surface-950 mb-1 mt-1">
1326
+ <Typography variant="caption" className="font-bold uppercase tracking-wider text-[9px] text-text-disabled dark:text-text-disabled-dark">{t("studio_sql_role")}</Typography>
1327
+ </div>
1328
+ {availableRoles.map(role => (
1329
+ <MenuItem key={role} dense onClick={() => handleRoleChange(role)} className={cls("text-xs", selectedRole === role && "text-primary dark:text-primary-dark")}>
1330
+ {role}{role === "postgres" ? " " + t("studio_sql_admin") : ""}
1331
+ </MenuItem>
1332
+ ))}
1333
+ </>
1334
+ )}
1335
+ </div>
1336
+ </Menu>
1337
+
1338
+ <Button
1339
+ onClick={() => handleRun()}
1340
+ disabled={loading}
1341
+ size="small"
1342
+ color="primary"
1343
+ >
1344
+ {loading ? <CircularProgress size="smallest" className="mr-2"/> : <PlayIcon size={iconSize.smallest} className="mr-2"/>}
1345
+ {t("studio_sql_run")}
1346
+ </Button>
1347
+ </div>
1348
+ </div>
1349
+
1350
+ <ResizablePanels
1351
+ orientation="vertical"
1352
+ panelSizePercent={editorHeight}
1353
+ onPanelSizeChange={setEditorHeight}
1354
+ minPanelSizePx={100}
1355
+ firstPanel={
1356
+ <div className="h-full w-full relative flex flex-col min-h-0">
1357
+ <MonacoEditor
1358
+ value={sql}
1359
+ onChange={(v) => setSql(v || "")}
1360
+ onRun={handleRun}
1361
+ schemas={schemas}
1362
+ />
1363
+ </div>
1364
+ }
1365
+ secondPanel={
1366
+ <div className="h-full w-full flex flex-col bg-surface-50 dark:bg-surface-950 overflow-hidden min-h-0">
1367
+ <div className={cls("p-2 px-4 bg-surface-100 dark:bg-surface-900 border-b shrink-0 flex items-center", defaultBorderMixin)}>
1368
+ <Typography variant="caption" className="font-bold text-text-disabled dark:text-text-disabled-dark uppercase tracking-widest text-[10px]">{t("studio_sql_query_results")}</Typography>
1369
+ </div>
1370
+ <div className="flex-grow flex flex-col min-h-0 overflow-hidden">
1371
+ {renderResults()}
1372
+ </div>
1373
+ </div>
1374
+ }
1375
+ />
1376
+
1377
+ </div>
1378
+ }
1379
+ />
1380
+
1381
+ <Dialog open={isSaveDialogOpen} onOpenChange={setIsSaveDialogOpen}>
1382
+ <DialogTitle>{t("studio_sql_save_snippet")}</DialogTitle>
1383
+ <DialogContent>
1384
+ <div className="py-4 flex flex-col gap-4">
1385
+ <TextField
1386
+ label={t("studio_sql_snippet_name")}
1387
+ autoFocus
1388
+ placeholder={t("studio_sql_snippet_name_placeholder")}
1389
+ value={newSnippetName}
1390
+ onChange={(e) => setNewSnippetName(e.target.value)}
1391
+ onKeyDown={(e) => {
1392
+ if (e.key === "Enter") {
1393
+ e.preventDefault();
1394
+ handleSaveSnippet();
1395
+ }
1396
+ }}
1397
+ />
1398
+ <Typography variant="caption" className="text-text-disabled dark:text-text-disabled-dark block">{t("studio_sql_snippet_saved_local")}</Typography>
1399
+ </div>
1400
+ </DialogContent>
1401
+ <DialogActions>
1402
+ <Button variant="text" onClick={() => setIsSaveDialogOpen(false)}>{t("studio_sql_cancel")}</Button>
1403
+ <Button onClick={handleSaveSnippet} color="primary" disabled={!newSnippetName.trim()}>{t("studio_sql_save")}</Button>
1404
+ </DialogActions>
1405
+ </Dialog>
1406
+ {/* Confirmation Dialog */}
1407
+ <ConfirmationDialog
1408
+ open={isConfirmDialogOpen}
1409
+ onCancel={() => setIsConfirmDialogOpen(false)}
1410
+ title={t("studio_sql_dangerous_operation")}
1411
+ body={t("studio_sql_dangerous_operation_body")}
1412
+ onAccept={() => {
1413
+ if (pendingAction) pendingAction();
1414
+ setIsConfirmDialogOpen(false);
1415
+ }}
1416
+ />
1417
+ </div>
1418
+ );
1419
+ };