mirror of
https://github.com/supabase/supabase.git
synced 2026-06-22 05:52:48 +08:00
* chore: move column priv * update docs * Minor fixes for column privileges. Add an explanation alert. * Add a right icon to productMenuItem component. * Rename the database replication into publications. * Change the order for the database menu. * Fix various links to db publications. * Remove duplicate entry. --------- Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
311 lines
12 KiB
TypeScript
311 lines
12 KiB
TypeScript
import * as Tooltip from '@radix-ui/react-tooltip'
|
|
import type { PostgresTable } from '@supabase/postgres-meta'
|
|
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { useParams } from 'common'
|
|
import { Lock, MousePointer2, PlusCircle } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { useState } from 'react'
|
|
import toast from 'react-hot-toast'
|
|
import { Button, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Popover_Shadcn_ } from 'ui'
|
|
|
|
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
|
import APIDocsButton from 'components/ui/APIDocsButton'
|
|
import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query'
|
|
import { useDatabasePublicationsQuery } from 'data/database-publications/database-publications-query'
|
|
import { useDatabasePublicationUpdateMutation } from 'data/database-publications/database-publications-update-mutation'
|
|
import { useTableUpdateMutation } from 'data/tables/table-update-mutation'
|
|
import { useCheckPermissions, useIsFeatureEnabled } from 'hooks'
|
|
import { EXCLUDED_SCHEMAS } from 'lib/constants/schemas'
|
|
import ConfirmModal from 'ui-patterns/Dialogs/ConfirmDialog'
|
|
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
|
import { RoleImpersonationPopover } from '../RoleImpersonationSelector'
|
|
|
|
export interface GridHeaderActionsProps {
|
|
table: PostgresTable
|
|
canEditViaTableEditor: boolean
|
|
isViewSelected: boolean
|
|
isTableSelected: boolean
|
|
}
|
|
|
|
const GridHeaderActions = ({ table, isViewSelected, isTableSelected }: GridHeaderActionsProps) => {
|
|
const { ref } = useParams()
|
|
const { project } = useProjectContext()
|
|
const realtimeEnabled = useIsFeatureEnabled('realtime:all')
|
|
const isLocked = EXCLUDED_SCHEMAS.includes(table.schema)
|
|
|
|
const { mutate: updateTable } = useTableUpdateMutation({
|
|
onError: (error) => {
|
|
toast.error(`Failed to toggle RLS: ${error.message}`)
|
|
},
|
|
onSettled: () => {
|
|
closeConfirmModal()
|
|
},
|
|
})
|
|
|
|
const [showEnableRealtime, setShowEnableRealtime] = useState(false)
|
|
const [open, setOpen] = useState(false)
|
|
const [rlsConfirmModalOpen, setRlsConfirmModalOpen] = useState(false)
|
|
|
|
const projectRef = project?.ref
|
|
const { data } = useDatabasePoliciesQuery({
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
})
|
|
const policies = (data ?? []).filter(
|
|
(policy) => policy.schema === table.schema && policy.table === table.name
|
|
)
|
|
|
|
const { data: publications } = useDatabasePublicationsQuery({
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
})
|
|
const realtimePublication = (publications ?? []).find(
|
|
(publication) => publication.name === 'supabase_realtime'
|
|
)
|
|
const realtimeEnabledTables = realtimePublication?.tables ?? []
|
|
const isRealtimeEnabled = realtimeEnabledTables.some((t: any) => t.id === table?.id)
|
|
|
|
const { mutate: updatePublications, isLoading: isTogglingRealtime } =
|
|
useDatabasePublicationUpdateMutation({
|
|
onSuccess: () => {
|
|
setShowEnableRealtime(false)
|
|
},
|
|
onError: (error) => {
|
|
toast.error(`Failed to toggle realtime for ${table.name}: ${error.message}`)
|
|
},
|
|
})
|
|
|
|
const canSqlWriteTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
|
|
const canSqlWriteColumns = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns')
|
|
const isReadOnly = !canSqlWriteTables && !canSqlWriteColumns
|
|
// This will change when we allow autogenerated API docs for schemas other than `public`
|
|
const doesHaveAutoGeneratedAPIDocs = table.schema === 'public'
|
|
|
|
const toggleRealtime = async () => {
|
|
if (!project) return console.error('Project is required')
|
|
if (!realtimePublication) return console.error('Unable to find realtime publication')
|
|
|
|
const exists = realtimeEnabledTables.some((x: any) => x.id == table.id)
|
|
const tables = !exists
|
|
? [`${table.schema}.${table.name}`].concat(
|
|
realtimeEnabledTables.map((t: any) => `${t.schema}.${t.name}`)
|
|
)
|
|
: realtimeEnabledTables
|
|
.filter((x: any) => x.id != table.id)
|
|
.map((x: any) => `${x.schema}.${x.name}`)
|
|
|
|
updatePublications({
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
id: realtimePublication.id,
|
|
tables,
|
|
})
|
|
}
|
|
|
|
const closeConfirmModal = () => {
|
|
setRlsConfirmModalOpen(false)
|
|
}
|
|
const onToggleRLS = async () => {
|
|
const payload = {
|
|
id: table.id,
|
|
rls_enabled: !table.rls_enabled,
|
|
}
|
|
|
|
updateTable({
|
|
projectRef: project?.ref!,
|
|
connectionString: project?.connectionString,
|
|
id: payload.id,
|
|
schema: table.schema,
|
|
payload: payload,
|
|
})
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="flex items-center gap-2">
|
|
{isReadOnly && (
|
|
<Tooltip.Root delayDuration={0}>
|
|
<Tooltip.Trigger className="w-full">
|
|
<div className="border border-strong rounded bg-overlay-hover px-3 py-1 text-xs">
|
|
Viewing as read-only
|
|
</div>
|
|
</Tooltip.Trigger>
|
|
<Tooltip.Portal>
|
|
<Tooltip.Content side="bottom">
|
|
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
|
<div
|
|
className={[
|
|
'rounded bg-alternative py-1 px-2 leading-none shadow',
|
|
'border border-background',
|
|
].join(' ')}
|
|
>
|
|
<span className="text-xs text-foreground">
|
|
You need additional permissions to manage your project's data
|
|
</span>
|
|
</div>
|
|
</Tooltip.Content>
|
|
</Tooltip.Portal>
|
|
</Tooltip.Root>
|
|
)}
|
|
|
|
{isTableSelected ? (
|
|
table.rls_enabled ? (
|
|
<>
|
|
{policies.length < 1 && !isLocked ? (
|
|
<Tooltip.Root delayDuration={0}>
|
|
<Tooltip.Trigger asChild className="w-full">
|
|
<Button
|
|
asChild
|
|
type="default"
|
|
className="group"
|
|
icon={<PlusCircle strokeWidth={1.5} className="text-foreground-muted" />}
|
|
>
|
|
<Link
|
|
passHref
|
|
href={`/project/${projectRef}/auth/policies?search=${table.id}&schema=${table.schema}`}
|
|
>
|
|
Add RLS policy
|
|
</Link>
|
|
</Button>
|
|
</Tooltip.Trigger>
|
|
<Tooltip.Portal>
|
|
<Tooltip.Content side="bottom">
|
|
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
|
<div
|
|
className={[
|
|
'rounded bg-alternative py-1 px-2 leading-none shadow',
|
|
'border border-background',
|
|
].join(' ')}
|
|
>
|
|
<div className="text-xs text-foreground p-1 leading-relaxed">
|
|
<p>RLS is enabled for this table, but no policies are set. </p>
|
|
<p>
|
|
Select queries will return an <u>empty array</u> of results.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Tooltip.Content>
|
|
</Tooltip.Portal>
|
|
</Tooltip.Root>
|
|
) : (
|
|
<Button
|
|
asChild
|
|
type={policies.length < 1 && !isLocked ? 'warning' : 'default'}
|
|
className="group"
|
|
icon={
|
|
isLocked || policies.length > 0 ? (
|
|
<span className="text-right text-xs rounded-xl px-[6px] bg-foreground-lighter/30 text-brand-1100">
|
|
{policies.length}
|
|
</span>
|
|
) : (
|
|
<PlusCircle strokeWidth={1.5} />
|
|
)
|
|
}
|
|
>
|
|
<Link
|
|
passHref
|
|
href={`/project/${projectRef}/auth/policies?search=${table.id}&schema=${table.schema}`}
|
|
>
|
|
Auth {policies.length > 1 ? 'policies' : 'policy'}
|
|
</Link>
|
|
</Button>
|
|
)}
|
|
</>
|
|
) : (
|
|
<Popover_Shadcn_ open={open} onOpenChange={() => setOpen(!open)} modal={false}>
|
|
<PopoverTrigger_Shadcn_ asChild>
|
|
<Button type="warning" icon={<Lock strokeWidth={1.5} />}>
|
|
RLS disabled
|
|
</Button>
|
|
</PopoverTrigger_Shadcn_>
|
|
<PopoverContent_Shadcn_ className="min-w-[395px] text-sm" align="end">
|
|
<h3 className="flex items-center gap-2">
|
|
<Lock size={16} /> Row Level Security (RLS)
|
|
</h3>
|
|
<div className="grid gap-2 mt-4 text-foreground-light text-sm">
|
|
<p>
|
|
You can restrict and control who can read, write and update data in this table
|
|
using Row Level Security.
|
|
</p>
|
|
<p>
|
|
With RLS enabled, anonymous users will not be able to read/write data in the
|
|
table.
|
|
</p>
|
|
{!isLocked && (
|
|
<div className="mt-2">
|
|
<Button
|
|
type="default"
|
|
onClick={() => setRlsConfirmModalOpen(!rlsConfirmModalOpen)}
|
|
>
|
|
Enable RLS for this table
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</PopoverContent_Shadcn_>
|
|
</Popover_Shadcn_>
|
|
)
|
|
) : null}
|
|
|
|
<RoleImpersonationPopover serviceRoleLabel="postgres" />
|
|
|
|
{realtimeEnabled && !isViewSelected && (
|
|
<Button
|
|
type="default"
|
|
icon={
|
|
<MousePointer2
|
|
strokeWidth={1.5}
|
|
className={isRealtimeEnabled ? 'text-brand' : 'text-foreground-muted'}
|
|
/>
|
|
}
|
|
onClick={() => setShowEnableRealtime(true)}
|
|
>
|
|
Realtime {isRealtimeEnabled ? 'on' : 'off'}
|
|
</Button>
|
|
)}
|
|
|
|
{doesHaveAutoGeneratedAPIDocs && <APIDocsButton section={['entities', table.name]} />}
|
|
</div>
|
|
|
|
<ConfirmationModal
|
|
visible={showEnableRealtime}
|
|
loading={isTogglingRealtime}
|
|
title={`${isRealtimeEnabled ? 'Disable' : 'Enable'} realtime for ${table.name}`}
|
|
confirmLabel={`${isRealtimeEnabled ? 'Disable' : 'Enable'} realtime`}
|
|
confirmLabelLoading={`${isRealtimeEnabled ? 'Disabling' : 'Enabling'} realtime`}
|
|
onCancel={() => setShowEnableRealtime(false)}
|
|
onConfirm={() => toggleRealtime()}
|
|
>
|
|
<div className="space-y-2">
|
|
<p className="text-sm">
|
|
Once realtime has been {isRealtimeEnabled ? 'disabled' : 'enabled'}, the table will{' '}
|
|
{isRealtimeEnabled ? 'no longer ' : ''}broadcast any changes to authorized subscribers.
|
|
</p>
|
|
{!isRealtimeEnabled && (
|
|
<p className="text-sm">
|
|
You may also select which events to broadcast to subscribers on the{' '}
|
|
<Link href={`/project/${ref}/database/publications`} className="text-brand">
|
|
database publications
|
|
</Link>{' '}
|
|
settings.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</ConfirmationModal>
|
|
|
|
<ConfirmModal
|
|
danger={table.rls_enabled}
|
|
visible={rlsConfirmModalOpen}
|
|
title="Confirm to enable Row Level Security"
|
|
description="Are you sure you want to enable Row Level Security for this table?"
|
|
buttonLabel="Enable RLS"
|
|
buttonLoadingLabel="Updating"
|
|
onSelectCancel={closeConfirmModal}
|
|
onSelectConfirm={onToggleRLS}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
export default GridHeaderActions
|