TableGridEditor.tsx 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import { PermissionAction } from '@supabase/shared-types/out/constants'
  2. import { useParams } from 'common'
  3. import Link from 'next/link'
  4. import { useRouter } from 'next/router'
  5. import { parseAsString, useQueryState } from 'nuqs'
  6. import { useCallback } from 'react'
  7. import { Button } from 'ui'
  8. import { Admonition, GenericSkeletonLoader } from 'ui-patterns'
  9. import DeleteConfirmationDialogs from './DeleteConfirmationDialogs'
  10. import { SidePanelEditor } from './SidePanelEditor/SidePanelEditor'
  11. import { TableDefinition } from './TableDefinition'
  12. import { BrivenGrid } from '@/components/grid/BrivenGrid'
  13. import { useSyncTableEditorStateFromLocalStorageWithUrl } from '@/components/grid/BrivenGrid.utils'
  14. import {
  15. Entity,
  16. isForeignTable,
  17. isMaterializedView,
  18. isTableLike,
  19. isView,
  20. TableLike,
  21. } from '@/data/table-editor/table-editor-types'
  22. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  23. import { useDashboardHistory } from '@/hooks/misc/useDashboardHistory'
  24. import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState'
  25. import { useIsProtectedSchema } from '@/hooks/useProtectedSchemas'
  26. import { TableEditorTableStateContextProvider } from '@/state/table-editor-table'
  27. import { createTabId, useTabsStateSnapshot } from '@/state/tabs'
  28. export interface TableGridEditorProps {
  29. isLoadingSelectedTable?: boolean
  30. selectedTable?: Entity
  31. }
  32. export const TableGridEditor = ({
  33. isLoadingSelectedTable = false,
  34. selectedTable,
  35. }: TableGridEditorProps) => {
  36. const router = useRouter()
  37. const { ref: projectRef, id } = useParams()
  38. const { setLastVisitedTable } = useDashboardHistory()
  39. const { selectedSchema } = useQuerySchemaState()
  40. const tabs = useTabsStateSnapshot()
  41. useSyncTableEditorStateFromLocalStorageWithUrl({
  42. projectRef,
  43. table: selectedTable,
  44. })
  45. const [selectedView] = useQueryState('view', parseAsString.withDefault('data'))
  46. const { can: canEditTables } = useAsyncCheckPermissions(
  47. PermissionAction.TENANT_SQL_ADMIN_WRITE,
  48. 'tables'
  49. )
  50. const { can: canEditColumns } = useAsyncCheckPermissions(
  51. PermissionAction.TENANT_SQL_ADMIN_WRITE,
  52. 'columns'
  53. )
  54. const isReadOnly = !canEditTables && !canEditColumns
  55. const tabId = !!id ? tabs.openTabs.find((x) => x.endsWith(id)) : undefined
  56. const openTabs = tabs.openTabs.filter((x) => !x.startsWith('sql'))
  57. const onTableCreated = useCallback(
  58. (table: { id: number }) => {
  59. router.push(
  60. `/project/${projectRef}/editor/${table.id}${!!selectedSchema ? `?schema=${selectedSchema}` : ''}`
  61. )
  62. },
  63. [projectRef, router, selectedSchema]
  64. )
  65. const onTableDeleted = useCallback(async () => {
  66. // For simplicity for now, we just open the first table within the same schema
  67. if (selectedTable) {
  68. // Close tab
  69. const tabId = createTabId(selectedTable.entity_type, { id: selectedTable.id })
  70. tabs.handleTabClose({
  71. id: tabId,
  72. router,
  73. editor: 'table',
  74. onClearDashboardHistory: () => setLastVisitedTable(undefined),
  75. })
  76. }
  77. }, [router, selectedTable, setLastVisitedTable, tabs])
  78. const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedTable?.schema ?? '' })
  79. // NOTE: DO NOT PUT HOOKS AFTER THIS LINE
  80. if (isLoadingSelectedTable || !projectRef) {
  81. return (
  82. <div className="flex flex-col">
  83. <div className="h-10 bg-dash-sidebar dark:bg-surface-100" />
  84. <div className="h-9 border-y" />
  85. <div className="p-2 col-span-full">
  86. <GenericSkeletonLoader />
  87. </div>
  88. </div>
  89. )
  90. }
  91. const isViewSelected = isView(selectedTable) || isMaterializedView(selectedTable)
  92. const isTableSelected = isTableLike(selectedTable)
  93. const isForeignTableSelected = isForeignTable(selectedTable)
  94. const canEditViaTableEditor = isTableSelected && !isSchemaLocked
  95. const editable = !isReadOnly && canEditViaTableEditor
  96. const gridKey = !!selectedTable
  97. ? `${selectedTable.schema}_${selectedTable.name}`
  98. : 'unknown-table'
  99. /** [Joshen] We're going to need to refactor BrivenGrid eventually to make the code here more readable
  100. * For context we previously built the BrivenGrid as a reusable npm component, but eventually decided
  101. * to just integrate it directly into the dashboard. The header, and body (+footer) should be decoupled.
  102. */
  103. return (
  104. // When any click happens in a table tab, the tab becomes permanent
  105. <div className="h-full" onClick={() => tabs.makeActiveTabPermanent()}>
  106. {!selectedTable ? (
  107. <div className="flex items-center justify-center h-full">
  108. <div className="w-[400px]">
  109. <Admonition
  110. type="default"
  111. title={`Unable to find your table with ID ${id}`}
  112. description="This table doesn't exist in your database"
  113. >
  114. {!!tabId ? (
  115. <Button
  116. type="default"
  117. className="mt-2"
  118. onClick={() => {
  119. tabs.handleTabClose({
  120. id: tabId,
  121. router,
  122. editor: 'table',
  123. onClearDashboardHistory: () => setLastVisitedTable(undefined),
  124. })
  125. }}
  126. >
  127. Close tab
  128. </Button>
  129. ) : openTabs.length > 0 ? (
  130. <Button
  131. asChild
  132. type="default"
  133. className="mt-2"
  134. onClick={() => setLastVisitedTable(undefined)}
  135. >
  136. <Link href={`/project/${projectRef}/editor/${openTabs[0].split('-')[1]}`}>
  137. Close tab
  138. </Link>
  139. </Button>
  140. ) : (
  141. <Button
  142. asChild
  143. type="default"
  144. className="mt-2"
  145. onClick={() => setLastVisitedTable(undefined)}
  146. >
  147. <Link href={`/project/${projectRef}/editor`}>Head back</Link>
  148. </Button>
  149. )}
  150. </Admonition>
  151. </div>
  152. </div>
  153. ) : (
  154. <TableEditorTableStateContextProvider
  155. key={`table-editor-table-${selectedTable.id}`}
  156. projectRef={projectRef}
  157. table={selectedTable}
  158. editable={editable}
  159. >
  160. <BrivenGrid
  161. key={gridKey}
  162. gridProps={{ height: '100%' }}
  163. customHeader={
  164. (isViewSelected || isTableSelected) && selectedView === 'definition' ? (
  165. <div className="px-2 flex items-center gap-x-2">
  166. <p>
  167. SQL Definition of <code className="text-sm">{selectedTable.name}</code>{' '}
  168. </p>
  169. <p className="text-foreground-light text-sm">(Read only)</p>
  170. </div>
  171. ) : null
  172. }
  173. >
  174. {(isViewSelected || isTableSelected) && selectedView === 'definition' && (
  175. <TableDefinition entity={selectedTable} />
  176. )}
  177. </BrivenGrid>
  178. <DeleteConfirmationDialogs
  179. selectedTable={isTableSelected ? selectedTable : undefined}
  180. onTableDeleted={onTableDeleted}
  181. />
  182. </TableEditorTableStateContextProvider>
  183. )}
  184. <SidePanelEditor
  185. editable={editable}
  186. selectedTable={
  187. isTableSelected || isForeignTableSelected ? (selectedTable as TableLike) : undefined
  188. }
  189. onTableCreated={onTableCreated}
  190. />
  191. </div>
  192. )
  193. }