This commit is contained in:
Danny White committed 2026-06-23 13:57:21 -06:00
1 parent 237b3ca5b3
commit b5a183c65e
24 files changed
+999 -362

No files matched your search

@@ -47,7 +47,7 @@ export const TableReplicationRow = ({
className="px-1.5"
icon={<ExternalLink />}
tooltip={{
content: { side: 'bottom', text: 'Open in Table Editor' },
content: { side: 'bottom', text: 'Table Editor' },
}}
>
<Link
@@ -162,6 +162,7 @@ export const ColumnList = ({
{!isSchemaLocked && isTableEntity && (
<ButtonTooltip
icon={<Plus />}
variant="default"
disabled={!canUpdateColumns}
onClick={() => onAddColumn()}
tooltip={{
@@ -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 (
<ColumnList
onAddColumn={onAddColumn}
onEditColumn={onEditColumn}
onDeleteColumn={onDeleteColumn}
/>
)
}
@@ -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 <GenericSkeletonLoader />
}
return (
<Card>
<CardContent>
<ApiAccessToggle
projectRef={project?.ref}
schemaName={table.schema}
tableName={table.name}
isNewRecord={false}
handler={handler}
hideHeading
/>
</CardContent>
</Card>
)
}
@@ -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 (
<Alert variant="destructive">
<CriticalIcon />
<AlertTitle>Once this table is deleted, it can no longer be restored</AlertTitle>
<AlertDescription>
Deleting <code className="text-code-inline">{table.schema}.{table.name}</code> permanently
removes the table and its data. Use cascade if dependent objects must be removed too.
</AlertDescription>
<AlertDescription className="mt-3">
<Button
variant="danger"
disabled={!canDeleteTables}
onClick={() => snap.onDeleteTable()}
>
Delete table
</Button>
</AlertDescription>
</Alert>
)
}
@@ -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 (
<>
<FormLayout
isReactForm={false}
label="Index Advisor"
description="Recommends indexes to improve query performance based on your query patterns."
layout="flex-row-reverse"
>
<Button
variant="default"
onClick={() => {
setEnableDialogOpen(true)
track('index_advisor_enable_button_clicked', { origin: 'table_settings' })
}}
>
Enable
</Button>
</FormLayout>
<EnableIndexAdvisorDialog open={enableDialogOpen} setOpen={setEnableDialogOpen} />
</>
)
}
if (isLoading) {
return <GenericSkeletonLoader />
}
const suggestionCount = columnsWithSuggestions.length
return (
<FormLayout
isReactForm={false}
label="Index Advisor"
description={
suggestionCount > 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"
>
<Button asChild variant="default">
<Link href={`/project/${ref}/observability/query-performance`}>View recommendations</Link>
</Button>
</FormLayout>
)
}
export function TableDetailIndexAdvisorSection({ table }: TableDetailIndexAdvisorSectionProps) {
const { isIndexAdvisorAvailable } = useIndexAdvisorStatus()
if (!isIndexAdvisorAvailable) return null
return (
<TableIndexAdvisorProvider schema={table.schema} table={table.name}>
<Card>
<CardContent>
<TableDetailIndexAdvisorContent />
</CardContent>
</Card>
</TableIndexAdvisorProvider>
)
}
@@ -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 (
<div className="flex flex-col gap-0.5">
<span className="text-xs text-foreground-lighter">{label}</span>
<div className="text-sm text-foreground">{children}</div>
</div>
)
}
export function TableDetailOverview({ entity, realtimeEnabled }: TableDetailOverviewProps) {
const isTable = isTableLike(entity)
return (
<div className="rounded-md border bg-surface-75 px-4 py-3">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<OverviewItem label="Schema">
<code className="text-code-inline">{entity.schema}</code>
</OverviewItem>
{isTable && (
<OverviewItem label="Row level security">
<div className="flex items-center gap-2">
{entity.rls_enabled ? (
<>
<Check size={14} className="text-brand-link" />
<span className="text-foreground-light">Enabled</span>
</>
) : (
<>
<X size={14} className="text-foreground-muted" />
<span className="text-foreground-lighter">Disabled</span>
</>
)}
</div>
</OverviewItem>
)}
{isTable && (
<OverviewItem label="Realtime">
<div className="flex items-center gap-2">
{realtimeEnabled ? (
<>
<Check size={14} className="text-brand-link" />
<span className="text-foreground-light">Enabled</span>
</>
) : (
<>
<X size={14} className="text-foreground-muted" />
<span className="text-foreground-lighter">Disabled</span>
</>
)}
</div>
</OverviewItem>
)}
{isTable && entity.size !== undefined && (
<OverviewItem label="Postgres size">
<span className="text-foreground-light">{entity.size}</span>
</OverviewItem>
)}
{entity.comment !== undefined && entity.comment !== null && entity.comment !== '' && (
<OverviewItem label="Comment">
<span className="text-foreground-light">{entity.comment}</span>
</OverviewItem>
)}
</div>
</div>
)
}
@@ -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 (
<>
<PageLayout
title={isLoading ? <ShimmeringLoader className="w-40" /> : (selectedTable?.name ?? '')}
breadcrumbs={[
{
label: 'Tables',
href: `/project/${ref}/database/tables`,
},
]}
size="large"
>
<PageContainer size="large" className="flex flex-col gap-8">
{selectedTable !== undefined && !isLoading && (
<TableDetailOverview entity={selectedTable} realtimeEnabled={realtimeEnabled} />
)}
{tableKey && selectedTable !== undefined && isTableLike(selectedTable) && (
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<div className="flex items-center gap-2">
<PageSectionTitle>Storage</PageSectionTitle>
{storageSyncError && <WarehouseSyncChip syncState="error" />}
</div>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<WarehouseTableStoragePanel tableKey={tableKey} postgresSize={selectedTable.size} />
</PageSectionContent>
</PageSection>
)}
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Columns</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<ColumnList
onAddColumn={snap.onAddColumn}
onEditColumn={snap.onEditColumn}
onDeleteColumn={snap.onDeleteColumn}
/>
</PageSectionContent>
</PageSection>
</PageContainer>
</PageLayout>
{project?.ref !== undefined && selectedTable !== undefined && isTableLike(selectedTable) && (
<TableEditorTableStateContextProvider
key={`table-editor-table-${selectedTable.id}`}
projectRef={project.ref}
table={selectedTable}
>
<DeleteConfirmationDialogs selectedTable={selectedTable} />
<SidePanelEditor includeColumns selectedTable={selectedTable} />
</TableEditorTableStateContextProvider>
)}
</>
)
}
@@ -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<Policy>()
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 <GenericSkeletonLoader />
}
if (isPoliciesError) {
return <AlertError error={policiesError} subject="Failed to retrieve policies" />
}
return (
<>
<PoliciesDataProvider
policies={policies ?? []}
isPoliciesLoading={isLoadingPolicies}
isPoliciesError={isPoliciesError}
policiesError={policiesError ?? undefined}
exposedSchemas={exposedSchemas}
>
<Policies
schema={table.schema}
tables={[policyTable]}
hasTables
isLocked={isSchemaLocked}
visibleTableIds={visibleTableIds}
onSelectCreatePolicy={() => {
setSelectedPolicyToEdit(undefined)
setShowPolicyEditor(true)
}}
onSelectEditPolicy={(policy) => {
setSelectedPolicyToEdit(policy)
setShowPolicyEditor(true)
}}
/>
</PoliciesDataProvider>
<PolicyEditorPanel
visible={showPolicyEditor}
searchString={String(table.id)}
schema={table.schema}
selectedPolicy={selectedPolicyToEdit}
onSelectCancel={() => setShowPolicyEditor(false)}
authContext="database"
/>
</>
)
}
@@ -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 (
<>
<div className="flex flex-col gap-8">
{showStorage && (
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Storage</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<WarehouseTableStoragePanel tableKey={tableKey} postgresSize={table.size} />
</PageSectionContent>
</PageSection>
)}
{isRealtimeFeatureEnabled && (
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Realtime</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Card>
<CardContent>
<FormLayout
isReactForm={false}
label="Realtime"
description="Broadcast changes on this table to authorized subscribers."
layout="flex-row-reverse"
>
<Button
variant="default"
icon={<Realtime size={14} className={isRealtimeEnabled ? 'text-brand' : ''} />}
onClick={() => setRealtimeDialogOpen(true)}
>
{isRealtimeEnabled ? 'Disable' : 'Enable'} realtime
</Button>
</FormLayout>
</CardContent>
</Card>
</PageSectionContent>
</PageSection>
)}
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Index Advisor</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<TableDetailIndexAdvisorSection table={table} />
</PageSectionContent>
</PageSection>
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Data API access</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<TableDetailDataApiSection table={table} />
</PageSectionContent>
</PageSection>
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Delete table</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<TableDetailDeleteTableSection table={table} />
</PageSectionContent>
</PageSection>
</div>
<RealtimeToggleDialog
table={table}
open={realtimeDialogOpen}
setOpen={setRealtimeDialogOpen}
/>
</>
)
}
@@ -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 (
<TableRow
key={x.id}
className="group cursor-pointer hover:bg-surface-100 inset-focus"
className="group relative cursor-pointer inset-focus"
onClick={handleRowNavigation}
onAuxClick={handleRowNavigation}
onKeyDown={handleRowNavigation}
@@ -625,7 +626,11 @@ export const TableList = ({
mode: 'postgres',
}
const mode = wState.mode as WarehouseMode
const warehouseSize = formatWarehouseSize(wState.warehouseSizeBytes)
const storageSummary = getWarehouseStorageSummaryLabel(
wState,
x.size
)
const storageUrl = `${tableDetailUrl}/settings`
const showSyncChip =
wState.syncState === 'syncing' || wState.syncState === 'error'
@@ -637,14 +642,12 @@ export const TableList = ({
return (
<Link
href={tableDetailUrl}
href={storageUrl}
onClick={(event: MouseEvent) => 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"
>
<span className="text-sm text-foreground-light">
{mode === 'warehouse_backed'
? `${warehouseSize} · Moved`
: `${x.size ?? '—'} · Copy · ${warehouseSize}`}
{storageSummary}
</span>
{showSyncChip && (
<WarehouseSyncChip syncState={wState.syncState!} />
@@ -673,8 +676,9 @@ export const TableList = ({
</TableCell>
<TableCell>
<div
className="flex justify-end gap-2"
className="flex items-center justify-end gap-3"
onClick={(event) => event.stopPropagation()}
onKeyDown={(event) => event.stopPropagation()}
>
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -730,7 +734,7 @@ export const TableList = ({
}}
>
<Edit size={12} />
<p>Edit table</p>
<p>Edit definitions</p>
</DropdownMenuItemTooltip>
<DropdownMenuItemTooltip
key="duplicate-table"
@@ -773,6 +777,11 @@ export const TableList = ({
)}
</DropdownMenuContent>
</DropdownMenu>
<ChevronRight
aria-hidden
size={14}
className="text-foreground-muted/60"
/>
</div>
</TableCell>
</TableRow>
@@ -95,19 +95,10 @@ export function WarehouseTableStoragePanel({
{mode === 'has_warehouse_copy' && (
<>
{state.syncState && (
<MetaRow label="Status">
<MetaRow label="Sync status">
<WarehouseSyncChip syncState={state.syncState} />
</MetaRow>
)}
{state.lastSyncedAt !== undefined && (
<MetaRow label="Last synced">
<TimestampInfo
className="text-sm text-foreground-light"
utcTimestamp={state.lastSyncedAt}
displayAs="local"
/>
</MetaRow>
)}
{state.lagSeconds !== undefined && (
<MetaRow label="Lag">
<span className="text-foreground-light">{state.lagSeconds}s</span>
@@ -165,15 +156,39 @@ export function WarehouseTableStoragePanel({
)}
{mode === 'has_warehouse_copy' && (
<Button type="button" variant="default" onClick={() => setDetachConfirm(true)}>
Detach Warehouse copy
</Button>
<div className="flex">
<Button
type="button"
variant="default"
className="rounded-r-none"
onClick={() => setEnablementModal('move')}
>
Move fully to Warehouse
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="default"
icon={<ChevronDown />}
className="rounded-l-none border-l-0 px-2"
aria-label="More storage actions"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setDetachConfirm(true)}>
Detach Warehouse copy
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
{mode === 'warehouse_backed' && (
<Button
type="button"
variant="default"
className="w-fit"
icon={<History />}
onClick={() => setTimeTravelOpen(true)}
>
@@ -86,6 +86,20 @@ export function formatWarehouseSize(bytes: number | undefined): string {
return `${Math.round(gb)} GB`
}
export function getWarehouseStorageSummaryLabel(
state: Pick<WarehouseTableState, 'mode' | 'warehouseSizeBytes'> | 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 []
@@ -19,7 +19,7 @@ export const IndexAdvisorNotice = () => {
if (!isIndexAdvisorAvailable || isIndexAdvisorEnabled || isDismissed) return null
return (
<div className="px-6">
<div>
<Admonition showIcon={false} type="tip" className="relative overflow-hidden mb-4">
<div className="absolute -inset-16 z-0 opacity-50">
<img
@@ -210,11 +210,12 @@ export const SidePanelEditor = ({
snap,
selectedTable,
})
const isTableDefinitionEdit = tableApiAccessParams?.type === 'edit'
const apiAccessToggleHandler = useTableApiAccessHandlerWithHistory(
// Dummy params used to appease TypeScript, actually gated by enabled flag
tableApiAccessParams ?? DUMMY_TABLE_API_ACCESS_PARAMS,
{
enabled: tableApiAccessParams !== undefined,
enabled: tableApiAccessParams !== undefined && !isTableDefinitionEdit,
}
)
@@ -600,7 +601,7 @@ export const SidePanelEditor = ({
let toastId
let saveTableError = false
if (!apiAccessToggleHandler.isSuccess) {
if (action !== 'update' && !apiAccessToggleHandler.isSuccess) {
if (apiAccessToggleHandler.isPending) {
toast.info(
'Cannot save table yet because Data API settings are still loading. Please try again in a moment.'
@@ -819,15 +820,7 @@ export const SidePanelEditor = ({
if (table === undefined) {
return toast.error('Failed to update table')
}
if (isTableLike(table)) {
await updateTableRealtime(table, isRealtimeEnabled)
const privilegesToSet = apiAccessToggleHandler.data?.schemaExposed
? apiAccessToggleHandler.data.privileges
: undefined
if (privilegesToSet) {
await updateTableApiAccess(table, privilegesToSet)
}
}
// Realtime and Data API access are managed on the table details Settings tab.
if (hasError) {
toast.warning(
@@ -278,6 +278,7 @@ type ApiAccessToggleComponentProps = {
tableName?: string
isNewRecord: boolean
handler: TableApiAccessHandlerWithHistoryReturn
hideHeading?: boolean
}
export const ApiAccessToggle = ({
@@ -286,6 +287,7 @@ export const ApiAccessToggle = ({
tableName,
isNewRecord,
handler,
hideHeading = false,
}: ApiAccessToggleComponentProps): ReactNode => {
const track = useTrack()
const isPending = handler.isPending
@@ -320,7 +322,7 @@ export const ApiAccessToggle = ({
<div className="space-y-3">
<div className="flex items-center justify-between gap-3">
<div>
<h5>Data API access</h5>
{!hideHeading && <h5>Data API access</h5>}
<p className="text-sm text-foreground-lighter">
Allow this table to be queried via Supabase client libraries or the API directly
</p>
@@ -21,7 +21,7 @@ export const HeaderTitle = ({ schema, table, isDuplicating }: HeaderTitleProps)
}
return (
<span>
Update table <code className="text-code-inline text-sm!">{table?.name}</code>
Edit definitions for <code className="text-code-inline text-sm!">{table?.name}</code>
</span>
)
}
@@ -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<PlainObject>({})
@@ -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 = ({
</FormItemLayout>
</SidePanel.Content>
<SidePanel.Separator />
{!isDefinitionEdit && (
<>
<SidePanel.Separator />
<SidePanel.Content className="space-y-10 py-6">
<div className="items-top flex space-x-2">
<Checkbox
id="enable-rls"
checked={tableFields.isRLSEnabled}
onCheckedChange={() => {
// 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 })
}}
/>
<div className="grid gap-1.5 leading-none">
<label
htmlFor="enable-rls"
className="text-sm text-foreground-light flex items-center space-x-2 leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
<span>Enable Row Level Security (RLS)</span>
<Badge>Recommended</Badge>
</label>
<p className="text-sm text-foreground-muted">
Restrict access to your table by enabling RLS and writing Postgres policies.
</p>
</div>
</div>
{tableFields.isRLSEnabled ? (
<Admonition
type="default"
className="mt-3!"
title="Policies are required to query data"
description={
<>
You need to create an access policy before you can query data from this table.
Without a policy, querying this table will return an{' '}
<u className="text-foreground">empty array</u> of results.{' '}
{isNewRecord ? 'You can create policies after saving this table.' : ''}
</>
}
>
<DocsButton
abbrev={false}
className="mt-2 w-min"
href={`${DOCS_URL}${docsRowLevelSecurityGuidePath}`}
/>
</Admonition>
) : (
<Admonition
type="warning"
className="mt-3!"
title="You are allowing anonymous access to your table"
description={
<>
{tableFields.name ? `The table ${tableFields.name}` : 'Your table'} will be
publicly writable and readable
</>
}
>
<DocsButton
abbrev={false}
className="mt-2 w-min"
href={`${DOCS_URL}${docsRowLevelSecurityGuidePath}`}
/>
</Admonition>
)}
{realtimeEnabled && (
<div className="items-top flex space-x-2">
<Checkbox
id="enable-realtime"
checked={tableFields.isRealtimeEnabled}
onCheckedChange={() => {
track('realtime_toggle_table_clicked', {
newState: tableFields.isRealtimeEnabled ? 'disabled' : 'enabled',
origin: 'tableSidePanel',
})
onUpdateField({
isRealtimeEnabled: !tableFields.isRealtimeEnabled,
})
}}
/>
<div className="grid gap-1.5 leading-none">
<label
htmlFor="enable-realtime"
className="text-sm text-foreground-light flex items-center space-x-2 leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Enable Realtime
</label>
<p className="text-sm text-foreground-muted">
Broadcast changes on this table to authorized subscribers.
</p>
<SidePanel.Content className="space-y-10 py-6">
<div className="items-top flex space-x-2">
<Checkbox
id="enable-rls"
checked={tableFields.isRLSEnabled}
onCheckedChange={() => {
// 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 })
}}
/>
<div className="grid gap-1.5 leading-none">
<label
htmlFor="enable-rls"
className="text-sm text-foreground-light flex items-center space-x-2 leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
<span>Enable Row Level Security (RLS)</span>
<Badge>Recommended</Badge>
</label>
<p className="text-sm text-foreground-muted">
Restrict access to your table by enabling RLS and writing Postgres policies.
</p>
</div>
</div>
</div>
)}
</SidePanel.Content>
{tableFields.isRLSEnabled ? (
<Admonition
type="default"
className="mt-3!"
title="Policies are required to query data"
description={
<>
You need to create an access policy before you can query data from this table.
Without a policy, querying this table will return an{' '}
<u className="text-foreground">empty array</u> of results.{' '}
{isNewRecord ? 'You can create policies after saving this table.' : ''}
</>
}
>
<DocsButton
abbrev={false}
className="mt-2 w-min"
href={`${DOCS_URL}${docsRowLevelSecurityGuidePath}`}
/>
</Admonition>
) : (
<Admonition
type="warning"
className="mt-3!"
title="You are allowing anonymous access to your table"
description={
<>
{tableFields.name ? `The table ${tableFields.name}` : 'Your table'} will be
publicly writable and readable
</>
}
>
<DocsButton
abbrev={false}
className="mt-2 w-min"
href={`${DOCS_URL}${docsRowLevelSecurityGuidePath}`}
/>
</Admonition>
)}
{realtimeEnabled && (
<div className="items-top flex space-x-2">
<Checkbox
id="enable-realtime"
checked={tableFields.isRealtimeEnabled}
onCheckedChange={() => {
track('realtime_toggle_table_clicked', {
newState: tableFields.isRealtimeEnabled ? 'disabled' : 'enabled',
origin: 'tableSidePanel',
})
onUpdateField({
isRealtimeEnabled: !tableFields.isRealtimeEnabled,
})
}}
/>
<div className="grid gap-1.5 leading-none">
<label
htmlFor="enable-realtime"
className="text-sm text-foreground-light flex items-center space-x-2 leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Enable Realtime
</label>
<p className="text-sm text-foreground-muted">
Broadcast changes on this table to authorized subscribers.
</p>
</div>
</div>
)}
</SidePanel.Content>
</>
)}
<SidePanel.Separator />
@@ -613,18 +616,22 @@ export const TableEditor = ({
</>
)}
<SidePanel.Separator />
<SidePanel.Content className="py-6 space-y-6">
<ApiAccessToggle
projectRef={project?.ref}
schemaName={isNewRecord ? selectedSchema : table?.schema}
tableName={
isNewRecord || isDuplicating ? tableFields.name : tableFields.name || table?.name
}
isNewRecord={isNewRecord || isDuplicating}
handler={apiAccessToggleHandler}
/>
</SidePanel.Content>
{!isDefinitionEdit && (
<>
<SidePanel.Separator />
<SidePanel.Content className="py-6 space-y-6">
<ApiAccessToggle
projectRef={project?.ref}
schemaName={isNewRecord ? selectedSchema : table?.schema}
tableName={
isNewRecord || isDuplicating ? tableFields.name : tableFields.name || table?.name
}
isNewRecord={isNewRecord || isDuplicating}
handler={apiAccessToggleHandler}
/>
</SidePanel.Content>
</>
)}
</>
</SidePanel>
)
@@ -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<TableDetailLayoutProps>) {
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 (
<DatabaseLayout title="Tables">
<div className="flex min-h-full w-full flex-col items-stretch">
<PageHeader size="full" className="sticky top-0 z-10 bg-surface-75">
<PageHeaderBreadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink asChild>
<Link href={`/project/${ref}/database/tables`}>Tables</Link>
</BreadcrumbLink>
</BreadcrumbItem>
{selectedTable && (
<>
<BreadcrumbSeparator />
<BreadcrumbItem>
<span>{selectedTable.name}</span>
</BreadcrumbItem>
</>
)}
</BreadcrumbList>
</PageHeaderBreadcrumb>
<PageHeaderMeta className="mb-4">
<PageHeaderSummary>
<div className="flex items-center gap-2">
{selectedTable && <EntityTypeIcon type={selectedTable.entity_type} />}
<PageHeaderTitle>
{isLoading ? <ShimmeringLoader className="w-40" /> : (selectedTable?.name ?? '')}
</PageHeaderTitle>
{storageSyncError && <WarehouseSyncChip syncState="error" />}
</div>
{selectedTable && !isLoading && (
<PageHeaderDescription className="flex flex-col gap-1 text-sm!">
{selectedTable.comment && (
<p className="text-foreground-lighter mb-1">{selectedTable.comment}</p>
)}
<div className="flex flex-row flex-wrap items-center gap-x-4 gap-y-1 text-foreground-light">
<code className="text-code-inline">{selectedTable.schema}</code>
{isTable && selectedTable.live_rows_estimate !== undefined && (
<span>{selectedTable.live_rows_estimate.toLocaleString()} rows</span>
)}
{tableSizeLabel && <span>{tableSizeLabel}</span>}
{isTable && (
<span className="inline-flex items-center gap-1">
RLS
{selectedTable.rls_enabled ? (
<Check size={14} className="text-brand-link" />
) : (
<X size={14} className="text-foreground-muted" />
)}
</span>
)}
{isTable && (
<span className="inline-flex items-center gap-1">
Realtime
{realtimeEnabled ? (
<Check size={14} className="text-brand-link" />
) : (
<X size={14} className="text-foreground-muted" />
)}
</span>
)}
</div>
</PageHeaderDescription>
)}
</PageHeaderSummary>
{isTable && tableEditorUrl && (
<PageHeaderAside>
<div className="flex items-center gap-2">
{canUpdateTables && (
<Button variant="default" icon={<Edit />} onClick={() => snap.onEditTable()}>
Edit
</Button>
)}
<Button
variant="default"
icon={<TableEditor size={16} strokeWidth={1.5} />}
asChild
>
<Link href={tableEditorUrl}>View</Link>
</Button>
</div>
</PageHeaderAside>
)}
</PageHeaderMeta>
{navigationItems.length > 1 && (
<PageHeaderNavigationTabs>
<NavMenu>
{navigationItems.map((item) => {
const isActive = router.asPath.split('?')[0] === item.href
return (
<NavMenuItem key={item.label} active={isActive}>
<Link href={item.href}>{item.label}</Link>
</NavMenuItem>
)
})}
</NavMenu>
</PageHeaderNavigationTabs>
)}
</PageHeader>
<PageContainer size={_section === 'settings' ? 'small' : 'large'} className="py-8">
{children}
</PageContainer>
</div>
{project?.ref !== undefined && selectedTable !== undefined && isTable && (
<TableEditorTableStateContextProvider
key={`table-editor-table-${selectedTable.id}`}
projectRef={project.ref}
table={selectedTable}
>
<DeleteConfirmationDialogs
selectedTable={selectedTable}
onTableDeleted={() => {
router.push(`/project/${ref}/database/tables`)
}}
/>
<SidePanelEditor includeColumns selectedTable={selectedTable} />
</TableEditorTableStateContextProvider>
)}
</DatabaseLayout>
)
}
@@ -356,7 +356,7 @@ export const EntityListItem = ({
}}
>
<Edit size={12} className="shrink-0" />
<span>Edit table</span>
<span>Edit definitions</span>
</DropdownMenuItem>
<DropdownMenuItem
key="duplicate-table"
@@ -1,16 +1,58 @@
import { TableDetailPageContent } from '@/components/interfaces/Database/Tables/TableDetailPageContent'
import DatabaseLayout from '@/components/layouts/DatabaseLayout/DatabaseLayout'
import { useParams } from 'common'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import { ColumnList } from '@/components/interfaces/Database/Tables/ColumnList'
import { TableDetailColumnsTab } from '@/components/interfaces/Database/Tables/TableDetailColumnsTab'
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 { useTableEditorStateSnapshot } from '@/state/table-editor'
import type { NextPageWithLayout } from '@/types'
const DatabaseTableDetailPage: NextPageWithLayout = () => {
return <TableDetailPageContent />
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 <ShimmeringLoader />
}
if (selectedTable === undefined) {
return null
}
if (isTableLike(selectedTable)) {
return (
<TableDetailColumnsTab
onAddColumn={snap.onAddColumn}
onEditColumn={snap.onEditColumn}
onDeleteColumn={snap.onDeleteColumn}
/>
)
}
return (
<ColumnList
onAddColumn={snap.onAddColumn}
onEditColumn={snap.onEditColumn}
onDeleteColumn={snap.onDeleteColumn}
/>
)
}
DatabaseTableDetailPage.getLayout = (page) => (
TableDetailColumnsPage.getLayout = (page) => (
<DefaultLayout>
<DatabaseLayout title="Tables">{page}</DatabaseLayout>
<TableDetailLayout section="columns">{page}</TableDetailLayout>
</DefaultLayout>
)
export default DatabaseTableDetailPage
export default TableDetailColumnsPage
@@ -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 <ShimmeringLoader />
}
if (selectedTable === undefined || !isTableLike(selectedTable)) {
return null
}
return <TableDetailPoliciesTab table={selectedTable} />
}
TableDetailPoliciesPage.getLayout = (page) => (
<DefaultLayout>
<TableDetailLayout section="policies">{page}</TableDetailLayout>
</DefaultLayout>
)
export default TableDetailPoliciesPage
@@ -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 <ShimmeringLoader />
}
if (selectedTable === undefined || !isTableLike(selectedTable)) {
return null
}
return <TableDetailSettingsTab table={selectedTable} />
}
TableDetailSettingsPage.getLayout = (page) => (
<DefaultLayout>
<TableDetailLayout section="settings">{page}</TableDetailLayout>
</DefaultLayout>
)
export default TableDetailSettingsPage
@@ -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) => (
<DefaultLayout>
<DatabaseLayout title="Tables">{page}</DatabaseLayout>
<TableDetailLayout section="settings">{page}</TableDetailLayout>
</DefaultLayout>
)