mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 10:55:06 +03:00
Add realtime toggle button in table editor
This commit is contained in:
1 parent
3e9f9c1464
commit
c78cc45401
1 file changed
+144
-61
@@ -1,13 +1,15 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import type { PostgresPolicy, PostgresTable } from '@supabase/postgres-meta'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useParams } from 'common'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import { useCheckPermissions, useStore } from 'hooks'
|
||||
import { Button, IconAlertCircle, IconCode, IconLock } from 'ui'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { RLS_ACKNOWLEDGED_KEY, rlsAcknowledgedKey } from 'components/grid/constants'
|
||||
import { Button, IconAlertCircle, IconCode, IconLock, IconZap, IconZapOff, Modal } from 'ui'
|
||||
|
||||
import { rlsAcknowledgedKey } from 'components/grid/constants'
|
||||
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
|
||||
import ConfirmationModal from 'components/ui/ConfirmationModal'
|
||||
import { useCheckPermissions, useStore } from 'hooks'
|
||||
|
||||
export interface GridHeaderActionsProps {
|
||||
table: PostgresTable
|
||||
@@ -22,11 +24,23 @@ const GridHeaderActions = ({
|
||||
setApiPreviewPanelOpen,
|
||||
refreshDocs,
|
||||
}: GridHeaderActionsProps) => {
|
||||
const { meta } = useStore()
|
||||
const { ref } = useParams()
|
||||
const { meta, ui } = useStore()
|
||||
const { project } = useProjectContext()
|
||||
|
||||
const [isTogglingRealtime, setIsTogglingRealtime] = useState(false)
|
||||
const [showEnableRealtime, setShowEnableRealtime] = useState(false)
|
||||
|
||||
const projectRef = project?.ref
|
||||
const publications = meta.publications.list()
|
||||
const policies = meta.policies.list((policy: PostgresPolicy) => policy.table_id === table.id)
|
||||
|
||||
const realtimePublication = publications.find(
|
||||
(publication) => publication.name === 'supabase_realtime'
|
||||
)
|
||||
const realtimeEnabledTables = realtimePublication?.tables ?? []
|
||||
const isRealtimeEnabled = realtimeEnabledTables.some((t: any) => t.id === table?.id)
|
||||
|
||||
const canSqlWriteTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
|
||||
const canSqlWriteColumns = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns')
|
||||
const isReadOnly = !canSqlWriteTables && !canSqlWriteColumns
|
||||
@@ -34,14 +48,11 @@ const GridHeaderActions = ({
|
||||
const doesHaveAutoGeneratedAPIdocs = table.schema === 'public'
|
||||
|
||||
const rlsKey = rlsAcknowledgedKey(table.id.toString())
|
||||
|
||||
const rlsAcknowledgedChannel = useRef(new BroadcastChannel(rlsKey))
|
||||
const [showRLSWarning, setShowRLSWarning] = useState(
|
||||
!table.rls_enabled && localStorage.getItem(rlsKey) === 'true'
|
||||
)
|
||||
|
||||
const rlsDisabledMessage = 'RLS is not enabled'
|
||||
|
||||
useEffect(() => {
|
||||
if (rlsAcknowledgedChannel.current) {
|
||||
rlsAcknowledgedChannel.current.close()
|
||||
@@ -75,62 +86,134 @@ const GridHeaderActions = ({
|
||||
)
|
||||
}
|
||||
|
||||
const toggleRealtime = async () => {
|
||||
if (!realtimePublication) return console.error('Unable to find realtime publication')
|
||||
|
||||
setIsTogglingRealtime(true)
|
||||
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}`)
|
||||
|
||||
try {
|
||||
const id = realtimePublication.id
|
||||
const payload = { tables, id }
|
||||
const { error } = await meta.publications.update(id, payload)
|
||||
if (error) throw error
|
||||
setShowEnableRealtime(false)
|
||||
} catch (error: any) {
|
||||
ui.setNotification({
|
||||
category: 'error',
|
||||
message: `Failed to toggle realtime for ${table.name}: ${error.message}`,
|
||||
})
|
||||
} finally {
|
||||
setIsTogglingRealtime(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-3">
|
||||
{isReadOnly && (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger className="w-full">
|
||||
<div className="border border-scale-700 rounded bg-scale-500 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-scale-100 py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
You need additional permissions to manage your project's data
|
||||
</span>
|
||||
<>
|
||||
<div className="flex items-center space-x-3">
|
||||
{isReadOnly && (
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger className="w-full">
|
||||
<div className="border border-scale-700 rounded bg-scale-500 px-3 py-1 text-xs">
|
||||
Viewing as read-only
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
|
||||
'border border-scale-200',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-scale-1200">
|
||||
You need additional permissions to manage your project's data
|
||||
</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
|
||||
{(table.rls_enabled || showRLSWarning) && (
|
||||
<Link href={`/project/${projectRef}/auth/policies?search=${table.id}`}>
|
||||
<a>
|
||||
<Button
|
||||
type={table.rls_enabled ? 'link' : 'warning'}
|
||||
icon={
|
||||
table.rls_enabled ? (
|
||||
<IconLock strokeWidth={2} size={14} />
|
||||
) : (
|
||||
<IconAlertCircle strokeWidth={2} size={14} />
|
||||
)
|
||||
}
|
||||
>
|
||||
{!table.rls_enabled
|
||||
? rlsDisabledMessage
|
||||
: `${policies.length == 0 ? 'No' : policies.length} active RLS polic${
|
||||
policies.length > 1 || policies.length == 0 ? 'ies' : 'y'
|
||||
}`}
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
{(table.rls_enabled || showRLSWarning) && (
|
||||
<Link href={`/project/${projectRef}/auth/policies?search=${table.id}`}>
|
||||
<a>
|
||||
<Button
|
||||
type={table.rls_enabled ? 'link' : 'warning'}
|
||||
icon={
|
||||
table.rls_enabled ? (
|
||||
<IconLock strokeWidth={2} size={14} />
|
||||
) : (
|
||||
<IconAlertCircle strokeWidth={2} size={14} />
|
||||
)
|
||||
}
|
||||
>
|
||||
{!table.rls_enabled
|
||||
? 'RLS is not enabled'
|
||||
: `${policies.length == 0 ? 'No' : policies.length} active RLS polic${
|
||||
policies.length > 1 || policies.length == 0 ? 'ies' : 'y'
|
||||
}`}
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{doesHaveAutoGeneratedAPIdocs && (
|
||||
<div className="mt-[1px]">
|
||||
<RenderAPIPreviewToggle />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="default"
|
||||
icon={
|
||||
isRealtimeEnabled ? (
|
||||
<IconZap className="text-brand" strokeWidth={1.5} />
|
||||
) : (
|
||||
<IconZapOff />
|
||||
)
|
||||
}
|
||||
onClick={() => setShowEnableRealtime(true)}
|
||||
>
|
||||
Realtime {isRealtimeEnabled ? 'enabled' : 'disabled'}
|
||||
</Button>
|
||||
|
||||
{doesHaveAutoGeneratedAPIdocs && (
|
||||
<div className="mt-[1px]">
|
||||
<RenderAPIPreviewToggle />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ConfirmationModal
|
||||
visible={showEnableRealtime}
|
||||
loading={isTogglingRealtime}
|
||||
header={`${isRealtimeEnabled ? 'Disable' : 'Enable'} realtime for ${table.name}`}
|
||||
buttonLabel={`${isRealtimeEnabled ? 'Disable' : 'Enable'} realtime`}
|
||||
buttonLoadingLabel={`${isRealtimeEnabled ? 'Disabling' : 'Enabling'} realtime`}
|
||||
onSelectCancel={() => setShowEnableRealtime(false)}
|
||||
onSelectConfirm={() => toggleRealtime()}
|
||||
>
|
||||
<Modal.Content className="py-4 space-y-2">
|
||||
<p className="text-sm">
|
||||
Any changes on this table will {isRealtimeEnabled && 'no longer '}be broadcasted to
|
||||
authorized subscribers once realtime has been{' '}
|
||||
{isRealtimeEnabled ? 'disabled' : 'enabled'}.
|
||||
</p>
|
||||
{!isRealtimeEnabled && (
|
||||
<p className="text-sm">
|
||||
You may also select which events to broadcast to subscribers on the{' '}
|
||||
<Link href={`/project/${ref}/database/replication`}>
|
||||
<a className="text-brand">database replication</a>
|
||||
</Link>{' '}
|
||||
settings.
|
||||
</p>
|
||||
)}
|
||||
</Modal.Content>
|
||||
</ConfirmationModal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user