diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/TableReplicationRow.tsx b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/TableReplicationRow.tsx
index 51bbe46529b..17f61d935c7 100644
--- a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/TableReplicationRow.tsx
+++ b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/TableReplicationRow.tsx
@@ -47,7 +47,7 @@ export const TableReplicationRow = ({
className="px-1.5"
icon={ }
tooltip={{
- content: { side: 'bottom', text: 'Open in Table Editor' },
+ content: { side: 'bottom', text: 'Table Editor' },
}}
>
}
+ variant="default"
disabled={!canUpdateColumns}
onClick={() => onAddColumn()}
tooltip={{
diff --git a/apps/studio/components/interfaces/Database/Tables/TableDetailColumnsTab.tsx b/apps/studio/components/interfaces/Database/Tables/TableDetailColumnsTab.tsx
new file mode 100644
index 00000000000..28d8a5de0e8
--- /dev/null
+++ b/apps/studio/components/interfaces/Database/Tables/TableDetailColumnsTab.tsx
@@ -0,0 +1,22 @@
+import { ColumnList } from './ColumnList'
+import type { SafePostgresColumn } from '@/lib/postgres-types'
+
+interface TableDetailColumnsTabProps {
+ onAddColumn: () => void
+ onEditColumn: (column: SafePostgresColumn) => void
+ onDeleteColumn: (column: SafePostgresColumn) => void
+}
+
+export function TableDetailColumnsTab({
+ onAddColumn,
+ onEditColumn,
+ onDeleteColumn,
+}: TableDetailColumnsTabProps) {
+ return (
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Database/Tables/TableDetailDataApiSection.tsx b/apps/studio/components/interfaces/Database/Tables/TableDetailDataApiSection.tsx
new file mode 100644
index 00000000000..61f47b2770f
--- /dev/null
+++ b/apps/studio/components/interfaces/Database/Tables/TableDetailDataApiSection.tsx
@@ -0,0 +1,41 @@
+import { Card, CardContent } from 'ui'
+import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
+
+import {
+ ApiAccessToggle,
+ useTableApiAccessHandlerWithHistory,
+} from '@/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/ApiAccessToggle'
+import type { TableLike } from '@/data/table-editor/table-editor-types'
+import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
+
+interface TableDetailDataApiSectionProps {
+ table: TableLike
+}
+
+export function TableDetailDataApiSection({ table }: TableDetailDataApiSectionProps) {
+ const { data: project } = useSelectedProjectQuery()
+ const handler = useTableApiAccessHandlerWithHistory({
+ type: 'edit',
+ schemaName: table.schema,
+ tableName: table.name,
+ })
+
+ if (handler.isPending) {
+ return
+ }
+
+ return (
+
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Database/Tables/TableDetailDeleteTableSection.tsx b/apps/studio/components/interfaces/Database/Tables/TableDetailDeleteTableSection.tsx
new file mode 100644
index 00000000000..d86a93b0e98
--- /dev/null
+++ b/apps/studio/components/interfaces/Database/Tables/TableDetailDeleteTableSection.tsx
@@ -0,0 +1,38 @@
+import { PermissionAction } from '@supabase/shared-types/out/constants'
+import { Alert, AlertDescription, AlertTitle, Button, CriticalIcon } from 'ui'
+
+import type { TableLike } from '@/data/table-editor/table-editor-types'
+import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
+import { useTableEditorStateSnapshot } from '@/state/table-editor'
+
+interface TableDetailDeleteTableSectionProps {
+ table: TableLike
+}
+
+export function TableDetailDeleteTableSection({ table }: TableDetailDeleteTableSectionProps) {
+ const snap = useTableEditorStateSnapshot()
+ const { can: canDeleteTables } = useAsyncCheckPermissions(
+ PermissionAction.TENANT_SQL_ADMIN_WRITE,
+ 'tables'
+ )
+
+ return (
+
+
+ Once this table is deleted, it can no longer be restored
+
+ Deleting {table.schema}.{table.name} permanently
+ removes the table and its data. Use cascade if dependent objects must be removed too.
+
+
+ snap.onDeleteTable()}
+ >
+ Delete table
+
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Database/Tables/TableDetailIndexAdvisorSection.tsx b/apps/studio/components/interfaces/Database/Tables/TableDetailIndexAdvisorSection.tsx
new file mode 100644
index 00000000000..c9176f56f3b
--- /dev/null
+++ b/apps/studio/components/interfaces/Database/Tables/TableDetailIndexAdvisorSection.tsx
@@ -0,0 +1,92 @@
+import { useParams } from 'common'
+import Link from 'next/link'
+import { useState } from 'react'
+import { Button, Card, CardContent } from 'ui'
+import { FormLayout } from 'ui-patterns/form/Layout/FormLayout'
+import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
+
+import {
+ TableIndexAdvisorProvider,
+ useTableIndexAdvisor,
+} from '@/components/grid/context/TableIndexAdvisorContext'
+import { EnableIndexAdvisorDialog } from '@/components/interfaces/QueryPerformance/IndexAdvisor/EnableIndexAdvisorButton'
+import { useIndexAdvisorStatus } from '@/components/interfaces/QueryPerformance/hooks/useIsIndexAdvisorStatus'
+import { useTrack } from '@/lib/telemetry/track'
+import type { TableLike } from '@/data/table-editor/table-editor-types'
+
+interface TableDetailIndexAdvisorSectionProps {
+ table: TableLike
+}
+
+function TableDetailIndexAdvisorContent() {
+ const track = useTrack()
+ const { ref } = useParams()
+ const [enableDialogOpen, setEnableDialogOpen] = useState(false)
+ const { isIndexAdvisorAvailable, isIndexAdvisorEnabled } = useIndexAdvisorStatus()
+ const { columnsWithSuggestions, isLoading } = useTableIndexAdvisor()
+
+ if (!isIndexAdvisorAvailable) return null
+
+ if (!isIndexAdvisorEnabled) {
+ return (
+ <>
+
+ {
+ setEnableDialogOpen(true)
+ track('index_advisor_enable_button_clicked', { origin: 'table_settings' })
+ }}
+ >
+ Enable
+
+
+
+ >
+ )
+ }
+
+ if (isLoading) {
+ return
+ }
+
+ const suggestionCount = columnsWithSuggestions.length
+
+ return (
+ 0
+ ? `${suggestionCount} column${suggestionCount === 1 ? '' : 's'} with index recommendations based on query patterns.`
+ : 'Enabled. Recommendations will appear as query patterns are analyzed.'
+ }
+ layout="flex-row-reverse"
+ >
+
+ View recommendations
+
+
+ )
+}
+
+export function TableDetailIndexAdvisorSection({ table }: TableDetailIndexAdvisorSectionProps) {
+ const { isIndexAdvisorAvailable } = useIndexAdvisorStatus()
+
+ if (!isIndexAdvisorAvailable) return null
+
+ return (
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/Database/Tables/TableDetailOverview.tsx b/apps/studio/components/interfaces/Database/Tables/TableDetailOverview.tsx
deleted file mode 100644
index 2b2606ac622..00000000000
--- a/apps/studio/components/interfaces/Database/Tables/TableDetailOverview.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import { Check, X } from 'lucide-react'
-import type { ReactNode } from 'react'
-
-import type { Entity } from '@/data/table-editor/table-editor-types'
-import { isTableLike } from '@/data/table-editor/table-editor-types'
-
-interface TableDetailOverviewProps {
- entity: Entity
- realtimeEnabled: boolean
-}
-
-function OverviewItem({ label, children }: { label: string; children: ReactNode }) {
- return (
-
- )
-}
-
-export function TableDetailOverview({ entity, realtimeEnabled }: TableDetailOverviewProps) {
- const isTable = isTableLike(entity)
-
- return (
-
-
-
- {entity.schema}
-
-
- {isTable && (
-
-
- {entity.rls_enabled ? (
- <>
-
- Enabled
- >
- ) : (
- <>
-
- Disabled
- >
- )}
-
-
- )}
-
- {isTable && (
-
-
- {realtimeEnabled ? (
- <>
-
- Enabled
- >
- ) : (
- <>
-
- Disabled
- >
- )}
-
-
- )}
-
- {isTable && entity.size !== undefined && (
-
- {entity.size}
-
- )}
-
- {entity.comment !== undefined && entity.comment !== null && entity.comment !== '' && (
-
- {entity.comment}
-
- )}
-
-
- )
-}
diff --git a/apps/studio/components/interfaces/Database/Tables/TableDetailPageContent.tsx b/apps/studio/components/interfaces/Database/Tables/TableDetailPageContent.tsx
deleted file mode 100644
index a4f168795e3..00000000000
--- a/apps/studio/components/interfaces/Database/Tables/TableDetailPageContent.tsx
+++ /dev/null
@@ -1,121 +0,0 @@
-import { useParams } from 'common'
-import { PageContainer } from 'ui-patterns/PageContainer'
-import {
- PageSection,
- PageSectionContent,
- PageSectionMeta,
- PageSectionSummary,
- PageSectionTitle,
-} from 'ui-patterns/PageSection'
-import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
-import { useSnapshot } from 'valtio'
-
-import { ColumnList } from './ColumnList'
-import { TableDetailOverview } from './TableDetailOverview'
-import { warehouseDemoStore } from '@/components/interfaces/Database/Warehouse/warehouseDemoStore'
-import { WarehouseSyncChip } from '@/components/interfaces/Database/Warehouse/WarehouseSyncChip'
-import { WarehouseTableStoragePanel } from '@/components/interfaces/Database/Warehouse/WarehouseTableStoragePanel'
-import DeleteConfirmationDialogs from '@/components/interfaces/TableGridEditor/DeleteConfirmationDialogs'
-import { SidePanelEditor } from '@/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor'
-import { PageLayout } from '@/components/layouts/PageLayout/PageLayout'
-import { useDatabasePublicationsQuery } from '@/data/database-publications/database-publications-query'
-import { useTableEditorQuery } from '@/data/table-editor/table-editor-query'
-import { isTableLike } from '@/data/table-editor/table-editor-types'
-import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
-import { useTableEditorStateSnapshot } from '@/state/table-editor'
-import { TableEditorTableStateContextProvider } from '@/state/table-editor-table'
-
-export function TableDetailPageContent() {
- const snap = useTableEditorStateSnapshot()
- const { id: _id, ref } = useParams()
- const id = _id ? Number(_id) : undefined
-
- const { data: project } = useSelectedProjectQuery()
- const { data: selectedTable, isPending: isLoading } = useTableEditorQuery({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- id,
- })
-
- const { data: publications } = useDatabasePublicationsQuery({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- })
- const realtimePublication = (publications ?? []).find(
- (publication) => publication.name === 'supabase_realtime'
- )
- const realtimeEnabled =
- selectedTable?.id !== undefined &&
- (realtimePublication?.tables ?? []).some((table) => table.id === selectedTable.id)
-
- const warehouseSnap = useSnapshot(warehouseDemoStore)
- const tableKey =
- selectedTable?.schema !== undefined && selectedTable?.name !== undefined
- ? `${selectedTable.schema}.${selectedTable.name}`
- : undefined
- const warehouseState = tableKey ? warehouseSnap.tables[tableKey] : undefined
- const storageSyncError = warehouseState?.syncState === 'error'
-
- return (
- <>
- : (selectedTable?.name ?? '')}
- breadcrumbs={[
- {
- label: 'Tables',
- href: `/project/${ref}/database/tables`,
- },
- ]}
- size="large"
- >
-
- {selectedTable !== undefined && !isLoading && (
-
- )}
-
- {tableKey && selectedTable !== undefined && isTableLike(selectedTable) && (
-
-
-
-
-
Storage
- {storageSyncError &&
}
-
-
-
-
-
-
-
- )}
-
-
-
-
- Columns
-
-
-
-
-
-
-
-
-
- {project?.ref !== undefined && selectedTable !== undefined && isTableLike(selectedTable) && (
-
-
-
-
- )}
- >
- )
-}
diff --git a/apps/studio/components/interfaces/Database/Tables/TableDetailPoliciesTab.tsx b/apps/studio/components/interfaces/Database/Tables/TableDetailPoliciesTab.tsx
new file mode 100644
index 00000000000..5c90b51c8e6
--- /dev/null
+++ b/apps/studio/components/interfaces/Database/Tables/TableDetailPoliciesTab.tsx
@@ -0,0 +1,99 @@
+import { useMemo, useState } from 'react'
+import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
+
+import { Policies } from '@/components/interfaces/Auth/Policies/Policies'
+import { PoliciesDataProvider } from '@/components/interfaces/Auth/Policies/PoliciesDataContext'
+import { PolicyEditorPanel } from '@/components/interfaces/Auth/Policies/PolicyEditorPanel'
+import type { Policy } from '@/components/interfaces/Auth/Policies/PolicyTableRow/PolicyTableRow.utils'
+import { getExposedSchemas } from '@/components/layouts/ProjectNeedsSecuring/ProjectNeedsSecuring.utils'
+import AlertError from '@/components/ui/AlertError'
+import { useProjectPostgrestConfigQuery } from '@/data/config/project-postgrest-config-query'
+import { useDatabasePoliciesQuery } from '@/data/database-policies/database-policies-query'
+import type { TableLike } from '@/data/table-editor/table-editor-types'
+import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
+import { useIsProtectedSchema } from '@/hooks/useProtectedSchemas'
+
+interface TableDetailPoliciesTabProps {
+ table: TableLike
+}
+
+export function TableDetailPoliciesTab({ table }: TableDetailPoliciesTabProps) {
+ const { data: project } = useSelectedProjectQuery()
+ const isSchemaLocked = useIsProtectedSchema({ schema: table.schema }).isSchemaLocked
+
+ const [showPolicyEditor, setShowPolicyEditor] = useState(false)
+ const [selectedPolicyToEdit, setSelectedPolicyToEdit] = useState()
+
+ const { data: dbSchema } = useProjectPostgrestConfigQuery(
+ { projectRef: project?.ref },
+ { select: ({ db_schema }) => db_schema }
+ )
+ const exposedSchemas = useMemo(() => getExposedSchemas(dbSchema), [dbSchema])
+
+ const {
+ data: policies,
+ isPending: isLoadingPolicies,
+ isError: isPoliciesError,
+ error: policiesError,
+ } = useDatabasePoliciesQuery({
+ projectRef: project?.ref,
+ connectionString: project?.connectionString,
+ })
+
+ const visibleTableIds = useMemo(() => new Set([table.id]), [table.id])
+
+ const policyTable = useMemo(
+ () => ({
+ id: table.id,
+ schema: table.schema,
+ name: table.name,
+ rls_enabled: table.rls_enabled,
+ }),
+ [table]
+ )
+
+ if (isLoadingPolicies) {
+ return
+ }
+
+ if (isPoliciesError) {
+ return
+ }
+
+ return (
+ <>
+
+ {
+ setSelectedPolicyToEdit(undefined)
+ setShowPolicyEditor(true)
+ }}
+ onSelectEditPolicy={(policy) => {
+ setSelectedPolicyToEdit(policy)
+ setShowPolicyEditor(true)
+ }}
+ />
+
+
+ setShowPolicyEditor(false)}
+ authContext="database"
+ />
+ >
+ )
+}
diff --git a/apps/studio/components/interfaces/Database/Tables/TableDetailSettingsTab.tsx b/apps/studio/components/interfaces/Database/Tables/TableDetailSettingsTab.tsx
new file mode 100644
index 00000000000..986b370fec7
--- /dev/null
+++ b/apps/studio/components/interfaces/Database/Tables/TableDetailSettingsTab.tsx
@@ -0,0 +1,121 @@
+import { Realtime } from 'icons'
+import { useState } from 'react'
+import { Button, Card, CardContent } from 'ui'
+import { FormLayout } from 'ui-patterns/form/Layout/FormLayout'
+import {
+ PageSection,
+ PageSectionContent,
+ PageSectionMeta,
+ PageSectionSummary,
+ PageSectionTitle,
+} from 'ui-patterns/PageSection'
+
+import { TableDetailDataApiSection } from '@/components/interfaces/Database/Tables/TableDetailDataApiSection'
+import { TableDetailDeleteTableSection } from '@/components/interfaces/Database/Tables/TableDetailDeleteTableSection'
+import { TableDetailIndexAdvisorSection } from '@/components/interfaces/Database/Tables/TableDetailIndexAdvisorSection'
+import { WarehouseTableStoragePanel } from '@/components/interfaces/Database/Warehouse/WarehouseTableStoragePanel'
+import { RealtimeToggleDialog } from '@/components/interfaces/TableGridEditor/RealtimeToggleDialog'
+import { useIsTableRealtimeEnabled } from '@/data/database-publications/database-publications-query'
+import { ENTITY_TYPE } from '@/data/entity-types/entity-type-constants'
+import type { TableLike } from '@/data/table-editor/table-editor-types'
+import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
+
+interface TableDetailSettingsTabProps {
+ table: TableLike
+}
+
+export function TableDetailSettingsTab({ table }: TableDetailSettingsTabProps) {
+ const [realtimeDialogOpen, setRealtimeDialogOpen] = useState(false)
+ const { realtimeAll: isRealtimeFeatureEnabled } = useIsFeatureEnabled(['realtime:all'])
+ const isRealtimeEnabled = useIsTableRealtimeEnabled({ id: table.id })
+ const showStorage = table.entity_type === ENTITY_TYPE.TABLE
+ const tableKey = `${table.schema}.${table.name}`
+
+ return (
+ <>
+
+ {showStorage && (
+
+
+
+ Storage
+
+
+
+
+
+
+ )}
+
+ {isRealtimeFeatureEnabled && (
+
+
+
+ Realtime
+
+
+
+
+
+
+ }
+ onClick={() => setRealtimeDialogOpen(true)}
+ >
+ {isRealtimeEnabled ? 'Disable' : 'Enable'} realtime
+
+
+
+
+
+
+ )}
+
+
+
+
+ Index Advisor
+
+
+
+
+
+
+
+
+
+
+ Data API access
+
+
+
+
+
+
+
+
+
+
+ Delete table
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
diff --git a/apps/studio/components/interfaces/Database/Tables/TableList.tsx b/apps/studio/components/interfaces/Database/Tables/TableList.tsx
index 60f83a86df0..ae1035ef3e3 100644
--- a/apps/studio/components/interfaces/Database/Tables/TableList.tsx
+++ b/apps/studio/components/interfaces/Database/Tables/TableList.tsx
@@ -4,6 +4,7 @@ import { useParams } from 'common'
import { noop } from 'lodash'
import {
Check,
+ ChevronRight,
Columns2,
Copy,
Edit,
@@ -50,7 +51,7 @@ import { useSnapshot } from 'valtio'
import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning'
import {
- formatWarehouseSize,
+ getWarehouseStorageSummaryLabel,
warehouseDemoStore,
type WarehouseMode,
} from '../Warehouse/warehouseDemoStore'
@@ -560,7 +561,7 @@ export const TableList = ({
return (
event.stopPropagation()}
className="inline-flex max-w-full items-center gap-2 rounded-sm hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground-lighter"
>
- {mode === 'warehouse_backed'
- ? `${warehouseSize} · Moved`
- : `${x.size ?? '—'} · Copy · ${warehouseSize}`}
+ {storageSummary}
{showSyncChip && (
@@ -673,8 +676,9 @@ export const TableList = ({
event.stopPropagation()}
+ onKeyDown={(event) => event.stopPropagation()}
>
@@ -730,7 +734,7 @@ export const TableList = ({
}}
>
- Edit table
+ Edit definitions
+
diff --git a/apps/studio/components/interfaces/Database/Warehouse/WarehouseTableStoragePanel.tsx b/apps/studio/components/interfaces/Database/Warehouse/WarehouseTableStoragePanel.tsx
index 25744da0750..e7d4b570d22 100644
--- a/apps/studio/components/interfaces/Database/Warehouse/WarehouseTableStoragePanel.tsx
+++ b/apps/studio/components/interfaces/Database/Warehouse/WarehouseTableStoragePanel.tsx
@@ -95,19 +95,10 @@ export function WarehouseTableStoragePanel({
{mode === 'has_warehouse_copy' && (
<>
{state.syncState && (
-
+
)}
- {state.lastSyncedAt !== undefined && (
-
-
-
- )}
{state.lagSeconds !== undefined && (
{state.lagSeconds}s
@@ -165,15 +156,39 @@ export function WarehouseTableStoragePanel({
)}
{mode === 'has_warehouse_copy' && (
- setDetachConfirm(true)}>
- Detach Warehouse copy
-
+
+ setEnablementModal('move')}
+ >
+ Move fully to Warehouse
+
+
+
+ }
+ className="rounded-l-none border-l-0 px-2"
+ aria-label="More storage actions"
+ />
+
+
+ setDetachConfirm(true)}>
+ Detach Warehouse copy
+
+
+
+
)}
{mode === 'warehouse_backed' && (
}
onClick={() => setTimeTravelOpen(true)}
>
diff --git a/apps/studio/components/interfaces/Database/Warehouse/warehouseDemoStore.ts b/apps/studio/components/interfaces/Database/Warehouse/warehouseDemoStore.ts
index b27c895c298..e4d3cb01451 100644
--- a/apps/studio/components/interfaces/Database/Warehouse/warehouseDemoStore.ts
+++ b/apps/studio/components/interfaces/Database/Warehouse/warehouseDemoStore.ts
@@ -86,6 +86,20 @@ export function formatWarehouseSize(bytes: number | undefined): string {
return `${Math.round(gb)} GB`
}
+export function getWarehouseStorageSummaryLabel(
+ state: Pick | undefined,
+ postgresSize?: string
+): string | null {
+ const mode = state?.mode ?? 'postgres'
+ const warehouseSize = formatWarehouseSize(state?.warehouseSizeBytes)
+
+ if (mode === 'postgres') return null
+ if (mode === 'has_warehouse_copy') {
+ return postgresSize ? `${postgresSize} · Copy · ${warehouseSize}` : `Copy · ${warehouseSize}`
+ }
+ return `${warehouseSize} · Moved`
+}
+
export function getWarehouseSnapshots(tableKey: string): WarehouseSnapshot[] {
const state = warehouseDemoStore.tables[tableKey]
if (state?.mode !== 'warehouse_backed') return []
diff --git a/apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/IndexAdvisorNotice.tsx b/apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/IndexAdvisorNotice.tsx
index f2443402989..fb1b7465015 100644
--- a/apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/IndexAdvisorNotice.tsx
+++ b/apps/studio/components/interfaces/QueryPerformance/IndexAdvisor/IndexAdvisorNotice.tsx
@@ -19,7 +19,7 @@ export const IndexAdvisorNotice = () => {
if (!isIndexAdvisorAvailable || isIndexAdvisorEnabled || isDismissed) return null
return (
-
+
{
const track = useTrack()
const isPending = handler.isPending
@@ -320,7 +322,7 @@ export const ApiAccessToggle = ({
-
Data API access
+ {!hideHeading &&
Data API access }
Allow this table to be queried via Supabase client libraries or the API directly
diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/HeaderTitle.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/HeaderTitle.tsx
index 951205750c5..dc2becc5345 100644
--- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/HeaderTitle.tsx
+++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/HeaderTitle.tsx
@@ -21,7 +21,7 @@ export const HeaderTitle = ({ schema, table, isDuplicating }: HeaderTitleProps)
}
return (
- Update table {table?.name}
+ Edit definitions for {table?.name}
)
}
diff --git a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx
index c368d9af8ac..7e8420b1707 100644
--- a/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx
+++ b/apps/studio/components/interfaces/TableGridEditor/SidePanelEditor/TableEditor/TableEditor.tsx
@@ -84,6 +84,7 @@ export const TableEditor = ({
const { selectedSchema } = useQuerySchemaState()
const isNewRecord = table === undefined
+ const isDefinitionEdit = !isNewRecord && !isDuplicating
const visibleChanged = useChanged(visible)
const [errors, setErrors] = useState
({})
@@ -213,14 +214,13 @@ export const TableEditor = ({
const isNameChanged = tableFields.name.trim() !== table?.name
const isCommentChanged = tableFields.comment !== table?.comment
- const isRlsEnabledChanged = tableFields.isRLSEnabled !== (table?.rls_enabled ?? false)
if (isEmpty(errors)) {
const configuration = {
tableId: table?.id,
importContent,
- isRLSEnabled: tableFields.isRLSEnabled,
- isRealtimeEnabled: tableFields.isRealtimeEnabled,
+ isRLSEnabled: isDefinitionEdit ? (table?.rls_enabled ?? false) : tableFields.isRLSEnabled,
+ isRealtimeEnabled: isDefinitionEdit ? isRealtimeEnabled : tableFields.isRealtimeEnabled,
isDuplicateRows: isDuplicateRows,
existingForeignKeyRelations: foreignKeys,
primaryKey,
@@ -262,7 +262,6 @@ export const TableEditor = ({
const payload: SaveTablePayloadFor<'update'> = {
...(isNameChanged && { name: tableFields.name.trim() }),
...(isCommentChanged && { comment: tableFields.comment?.trim() ?? '' }),
- ...(isRlsEnabledChanged && { rls_enabled: tableFields.isRLSEnabled }),
}
saveChanges({
action: 'update',
@@ -434,104 +433,108 @@ export const TableEditor = ({
-
+ {!isDefinitionEdit && (
+ <>
+
-
-
-
{
- // if isEnabled, show confirm modal to turn off
- // if not enabled, allow turning on without modal confirmation
- tableFields.isRLSEnabled
- ? setRlsConfirmVisible(true)
- : onUpdateField({ isRLSEnabled: !tableFields.isRLSEnabled })
- }}
- />
-
-
- Enable Row Level Security (RLS)
- Recommended
-
-
- Restrict access to your table by enabling RLS and writing Postgres policies.
-
-
-
-
- {tableFields.isRLSEnabled ? (
-
- You need to create an access policy before you can query data from this table.
- Without a policy, querying this table will return an{' '}
- empty array of results.{' '}
- {isNewRecord ? 'You can create policies after saving this table.' : ''}
- >
- }
- >
-
-
- ) : (
-
- {tableFields.name ? `The table ${tableFields.name}` : 'Your table'} will be
- publicly writable and readable
- >
- }
- >
-
-
- )}
-
- {realtimeEnabled && (
-
-
{
- track('realtime_toggle_table_clicked', {
- newState: tableFields.isRealtimeEnabled ? 'disabled' : 'enabled',
- origin: 'tableSidePanel',
- })
- onUpdateField({
- isRealtimeEnabled: !tableFields.isRealtimeEnabled,
- })
- }}
- />
-
-
- Enable Realtime
-
-
- Broadcast changes on this table to authorized subscribers.
-
+
+
+
{
+ // if isEnabled, show confirm modal to turn off
+ // if not enabled, allow turning on without modal confirmation
+ tableFields.isRLSEnabled
+ ? setRlsConfirmVisible(true)
+ : onUpdateField({ isRLSEnabled: !tableFields.isRLSEnabled })
+ }}
+ />
+
+
+ Enable Row Level Security (RLS)
+ Recommended
+
+
+ Restrict access to your table by enabling RLS and writing Postgres policies.
+
+
-
- )}
-
+
+ {tableFields.isRLSEnabled ? (
+
+ You need to create an access policy before you can query data from this table.
+ Without a policy, querying this table will return an{' '}
+ empty array of results.{' '}
+ {isNewRecord ? 'You can create policies after saving this table.' : ''}
+ >
+ }
+ >
+
+
+ ) : (
+
+ {tableFields.name ? `The table ${tableFields.name}` : 'Your table'} will be
+ publicly writable and readable
+ >
+ }
+ >
+
+
+ )}
+
+ {realtimeEnabled && (
+
+
{
+ track('realtime_toggle_table_clicked', {
+ newState: tableFields.isRealtimeEnabled ? 'disabled' : 'enabled',
+ origin: 'tableSidePanel',
+ })
+ onUpdateField({
+ isRealtimeEnabled: !tableFields.isRealtimeEnabled,
+ })
+ }}
+ />
+
+
+ Enable Realtime
+
+
+ Broadcast changes on this table to authorized subscribers.
+
+
+
+ )}
+
+ >
+ )}
@@ -613,18 +616,22 @@ export const TableEditor = ({
>
)}
-
-
-
-
+ {!isDefinitionEdit && (
+ <>
+
+
+
+
+ >
+ )}
>
)
diff --git a/apps/studio/components/layouts/DatabaseLayout/TableDetailLayout.tsx b/apps/studio/components/layouts/DatabaseLayout/TableDetailLayout.tsx
new file mode 100644
index 00000000000..aa3567c78ad
--- /dev/null
+++ b/apps/studio/components/layouts/DatabaseLayout/TableDetailLayout.tsx
@@ -0,0 +1,265 @@
+import { PermissionAction } from '@supabase/shared-types/out/constants'
+import { useParams } from 'common'
+import { TableEditor } from 'icons'
+import { Check, Edit, X } from 'lucide-react'
+import Link from 'next/link'
+import { useRouter } from 'next/router'
+import React, { type PropsWithChildren } from 'react'
+import {
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbList,
+ BreadcrumbSeparator,
+ Button,
+ NavMenu,
+ NavMenuItem,
+} from 'ui'
+import { PageContainer } from 'ui-patterns/PageContainer'
+import {
+ PageHeader,
+ PageHeaderAside,
+ PageHeaderBreadcrumb,
+ PageHeaderDescription,
+ PageHeaderMeta,
+ PageHeaderNavigationTabs,
+ PageHeaderSummary,
+ PageHeaderTitle,
+} from 'ui-patterns/PageHeader'
+import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
+import { useSnapshot } from 'valtio'
+
+import DatabaseLayout from './DatabaseLayout'
+import { buildTableEditorUrl } from '@/components/grid/SupabaseGrid.utils'
+import {
+ getWarehouseStorageSummaryLabel,
+ warehouseDemoStore,
+} from '@/components/interfaces/Database/Warehouse/warehouseDemoStore'
+import { WarehouseSyncChip } from '@/components/interfaces/Database/Warehouse/WarehouseSyncChip'
+import DeleteConfirmationDialogs from '@/components/interfaces/TableGridEditor/DeleteConfirmationDialogs'
+import { SidePanelEditor } from '@/components/interfaces/TableGridEditor/SidePanelEditor/SidePanelEditor'
+import { EntityTypeIcon } from '@/components/ui/EntityTypeIcon'
+import { useDatabasePublicationsQuery } from '@/data/database-publications/database-publications-query'
+import { useTableEditorQuery } from '@/data/table-editor/table-editor-query'
+import { isTableLike } from '@/data/table-editor/table-editor-types'
+import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
+import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
+import { useTableEditorStateSnapshot } from '@/state/table-editor'
+import { TableEditorTableStateContextProvider } from '@/state/table-editor-table'
+
+export type TableDetailSection = 'columns' | 'policies' | 'settings'
+
+interface TableDetailLayoutProps {
+ section: TableDetailSection
+}
+
+export function TableDetailLayout({
+ section: _section,
+ children,
+}: PropsWithChildren) {
+ const router = useRouter()
+ const snap = useTableEditorStateSnapshot()
+ const { id: _id, ref } = useParams()
+ const id = _id ? Number(_id) : undefined
+
+ const { data: project } = useSelectedProjectQuery()
+ const { data: selectedTable, isPending: isLoading } = useTableEditorQuery({
+ projectRef: project?.ref,
+ connectionString: project?.connectionString,
+ id,
+ })
+
+ const { data: publications } = useDatabasePublicationsQuery({
+ projectRef: project?.ref,
+ connectionString: project?.connectionString,
+ })
+ const realtimePublication = (publications ?? []).find(
+ (publication) => publication.name === 'supabase_realtime'
+ )
+ const realtimeEnabled =
+ selectedTable?.id !== undefined &&
+ (realtimePublication?.tables ?? []).some((table) => table.id === selectedTable.id)
+
+ const warehouseSnap = useSnapshot(warehouseDemoStore)
+ const tableKey =
+ selectedTable?.schema !== undefined && selectedTable?.name !== undefined
+ ? `${selectedTable.schema}.${selectedTable.name}`
+ : undefined
+ const warehouseState = tableKey ? warehouseSnap.tables[tableKey] : undefined
+
+ const { can: canUpdateTables } = useAsyncCheckPermissions(
+ PermissionAction.TENANT_SQL_ADMIN_WRITE,
+ 'tables'
+ )
+
+ const isTable = selectedTable !== undefined && isTableLike(selectedTable)
+ const showPoliciesTab = isTable
+ const showSettingsTab = isTable
+ const storageSyncError = warehouseState?.syncState === 'error'
+ const tableSizeLabel =
+ isTable && selectedTable.size !== undefined
+ ? (getWarehouseStorageSummaryLabel(warehouseState, selectedTable.size) ?? selectedTable.size)
+ : isTable
+ ? getWarehouseStorageSummaryLabel(warehouseState, selectedTable.size)
+ : null
+
+ const tableEditorUrl =
+ selectedTable !== undefined
+ ? buildTableEditorUrl({
+ projectRef: ref,
+ tableId: selectedTable.id,
+ schema: selectedTable.schema,
+ })
+ : undefined
+
+ const navigationItems = id
+ ? [
+ {
+ label: 'Columns',
+ href: `/project/${ref}/database/tables/${id}`,
+ },
+ ...(showPoliciesTab
+ ? [
+ {
+ label: 'Policies',
+ href: `/project/${ref}/database/tables/${id}/policies`,
+ },
+ ]
+ : []),
+ ...(showSettingsTab
+ ? [
+ {
+ label: 'Settings',
+ href: `/project/${ref}/database/tables/${id}/settings`,
+ },
+ ]
+ : []),
+ ]
+ : []
+
+ return (
+
+
+
+
+
+
+
+ Tables
+
+
+ {selectedTable && (
+ <>
+
+
+ {selectedTable.name}
+
+ >
+ )}
+
+
+
+
+
+
+ {selectedTable &&
}
+
+ {isLoading ? : (selectedTable?.name ?? '')}
+
+ {storageSyncError &&
}
+
+
+ {selectedTable && !isLoading && (
+
+ {selectedTable.comment && (
+ {selectedTable.comment}
+ )}
+
+ {selectedTable.schema}
+ {isTable && selectedTable.live_rows_estimate !== undefined && (
+ {selectedTable.live_rows_estimate.toLocaleString()} rows
+ )}
+ {tableSizeLabel && {tableSizeLabel} }
+ {isTable && (
+
+ RLS
+ {selectedTable.rls_enabled ? (
+
+ ) : (
+
+ )}
+
+ )}
+ {isTable && (
+
+ Realtime
+ {realtimeEnabled ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ )}
+
+
+ {isTable && tableEditorUrl && (
+
+
+ {canUpdateTables && (
+ } onClick={() => snap.onEditTable()}>
+ Edit
+
+ )}
+ }
+ asChild
+ >
+ View
+
+
+
+ )}
+
+
+ {navigationItems.length > 1 && (
+
+
+ {navigationItems.map((item) => {
+ const isActive = router.asPath.split('?')[0] === item.href
+ return (
+
+ {item.label}
+
+ )
+ })}
+
+
+ )}
+
+
+
+ {children}
+
+
+
+ {project?.ref !== undefined && selectedTable !== undefined && isTable && (
+
+ {
+ router.push(`/project/${ref}/database/tables`)
+ }}
+ />
+
+
+ )}
+
+ )
+}
diff --git a/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx b/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx
index 6474dc81091..eb5d78d343c 100644
--- a/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx
+++ b/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx
@@ -356,7 +356,7 @@ export const EntityListItem = ({
}}
>
- Edit table
+ Edit definitions
{
- return
+const TableDetailColumnsPage: NextPageWithLayout = () => {
+ const snap = useTableEditorStateSnapshot()
+ const { id: _id } = useParams()
+ const id = _id ? Number(_id) : undefined
+ const { data: project } = useSelectedProjectQuery()
+ const { data: selectedTable, isPending } = useTableEditorQuery({
+ projectRef: project?.ref,
+ connectionString: project?.connectionString,
+ id,
+ })
+
+ if (isPending) {
+ return
+ }
+
+ if (selectedTable === undefined) {
+ return null
+ }
+
+ if (isTableLike(selectedTable)) {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
}
-DatabaseTableDetailPage.getLayout = (page) => (
+TableDetailColumnsPage.getLayout = (page) => (
- {page}
+ {page}
)
-export default DatabaseTableDetailPage
+export default TableDetailColumnsPage
diff --git a/apps/studio/pages/project/[ref]/database/tables/[id]/policies.tsx b/apps/studio/pages/project/[ref]/database/tables/[id]/policies.tsx
new file mode 100644
index 00000000000..28ce6ea8727
--- /dev/null
+++ b/apps/studio/pages/project/[ref]/database/tables/[id]/policies.tsx
@@ -0,0 +1,39 @@
+import { useParams } from 'common'
+import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
+
+import { TableDetailPoliciesTab } from '@/components/interfaces/Database/Tables/TableDetailPoliciesTab'
+import { TableDetailLayout } from '@/components/layouts/DatabaseLayout/TableDetailLayout'
+import DefaultLayout from '@/components/layouts/DefaultLayout'
+import { useTableEditorQuery } from '@/data/table-editor/table-editor-query'
+import { isTableLike } from '@/data/table-editor/table-editor-types'
+import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
+import type { NextPageWithLayout } from '@/types'
+
+const TableDetailPoliciesPage: NextPageWithLayout = () => {
+ const { id: _id } = useParams()
+ const id = _id ? Number(_id) : undefined
+ const { data: project } = useSelectedProjectQuery()
+ const { data: selectedTable, isPending } = useTableEditorQuery({
+ projectRef: project?.ref,
+ connectionString: project?.connectionString,
+ id,
+ })
+
+ if (isPending) {
+ return
+ }
+
+ if (selectedTable === undefined || !isTableLike(selectedTable)) {
+ return null
+ }
+
+ return
+}
+
+TableDetailPoliciesPage.getLayout = (page) => (
+
+ {page}
+
+)
+
+export default TableDetailPoliciesPage
diff --git a/apps/studio/pages/project/[ref]/database/tables/[id]/settings.tsx b/apps/studio/pages/project/[ref]/database/tables/[id]/settings.tsx
new file mode 100644
index 00000000000..5c1c8e41d67
--- /dev/null
+++ b/apps/studio/pages/project/[ref]/database/tables/[id]/settings.tsx
@@ -0,0 +1,39 @@
+import { useParams } from 'common'
+import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
+
+import { TableDetailSettingsTab } from '@/components/interfaces/Database/Tables/TableDetailSettingsTab'
+import { TableDetailLayout } from '@/components/layouts/DatabaseLayout/TableDetailLayout'
+import DefaultLayout from '@/components/layouts/DefaultLayout'
+import { useTableEditorQuery } from '@/data/table-editor/table-editor-query'
+import { isTableLike } from '@/data/table-editor/table-editor-types'
+import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
+import type { NextPageWithLayout } from '@/types'
+
+const TableDetailSettingsPage: NextPageWithLayout = () => {
+ const { id: _id } = useParams()
+ const id = _id ? Number(_id) : undefined
+ const { data: project } = useSelectedProjectQuery()
+ const { data: selectedTable, isPending } = useTableEditorQuery({
+ projectRef: project?.ref,
+ connectionString: project?.connectionString,
+ id,
+ })
+
+ if (isPending) {
+ return
+ }
+
+ if (selectedTable === undefined || !isTableLike(selectedTable)) {
+ return null
+ }
+
+ return
+}
+
+TableDetailSettingsPage.getLayout = (page) => (
+
+ {page}
+
+)
+
+export default TableDetailSettingsPage
diff --git a/apps/studio/pages/project/[ref]/database/tables/[id]/storage.tsx b/apps/studio/pages/project/[ref]/database/tables/[id]/storage.tsx
index 82678b8de00..357ba0d3abe 100644
--- a/apps/studio/pages/project/[ref]/database/tables/[id]/storage.tsx
+++ b/apps/studio/pages/project/[ref]/database/tables/[id]/storage.tsx
@@ -2,7 +2,7 @@ import { useParams } from 'common'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
-import DatabaseLayout from '@/components/layouts/DatabaseLayout/DatabaseLayout'
+import { TableDetailLayout } from '@/components/layouts/DatabaseLayout/TableDetailLayout'
import DefaultLayout from '@/components/layouts/DefaultLayout'
import type { NextPageWithLayout } from '@/types'
@@ -12,7 +12,7 @@ const TableStorageRedirect: NextPageWithLayout = () => {
useEffect(() => {
if (ref && id) {
- router.replace(`/project/${ref}/database/tables/${id}`)
+ router.replace(`/project/${ref}/database/tables/${id}/settings`)
}
}, [ref, id, router])
@@ -21,7 +21,7 @@ const TableStorageRedirect: NextPageWithLayout = () => {
TableStorageRedirect.getLayout = (page) => (
- {page}
+ {page}
)