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