| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425 |
- // @ts-nocheck
- import { PermissionAction } from '@supabase/shared-types/out/constants'
- import { useParams } from 'common'
- import { Realtime } from 'icons'
- import { BookOpenText, Lightbulb, Lock, MoreVertical, PlusCircle, Unlock } from 'lucide-react'
- import Link from 'next/link'
- import { parseAsBoolean, useQueryState } from 'nuqs'
- import { useState } from 'react'
- import { toast } from 'sonner'
- import {
- Button,
- cn,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
- Popover,
- PopoverContent,
- PopoverTrigger,
- Tooltip,
- TooltipContent,
- TooltipTrigger,
- } from 'ui'
- import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
- import { EnableIndexAdvisorDialog } from '../QueryPerformance/IndexAdvisor/EnableIndexAdvisorButton'
- import { RoleImpersonationPopover } from '../RoleImpersonationSelector/RoleImpersonationPopover'
- import { InsertButton } from './InsertButton'
- import { RealtimeToggleDialog } from './RealtimeToggleDialog'
- import { SecurityDefinerViewPopover } from './SecurityDefinerViewPopover'
- import { ViewEntityAutofixSecurityModal } from './ViewEntityAutofixSecurityModal'
- import { RefreshButton } from '@/components/grid/components/header/RefreshButton'
- import { useTableIndexAdvisor } from '@/components/grid/context/TableIndexAdvisorContext'
- import {
- getEntityLintDetails,
- getTablePoliciesUrl,
- } from '@/components/interfaces/TableGridEditor/TableEntity.utils'
- import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
- import { useDatabasePoliciesQuery } from '@/data/database-policies/database-policies-query'
- import { useIsTableRealtimeEnabled } from '@/data/database-publications/database-publications-query'
- import { useProjectLintsQuery } from '@/data/lint/lint-query'
- import {
- Entity,
- isTableLike,
- isForeignTable as isTableLikeForeignTable,
- isMaterializedView as isTableLikeMaterializedView,
- isView as isTableLikeView,
- } from '@/data/table-editor/table-editor-types'
- import { useTableUpdateMutation } from '@/data/tables/table-update-mutation'
- import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
- import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
- import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
- import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
- import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
- import { useIsProtectedSchema } from '@/hooks/useProtectedSchemas'
- import { DOCS_URL } from '@/lib/constants'
- import { useTrack } from '@/lib/telemetry/track'
- import { useAppStateSnapshot } from '@/state/app-state'
- import { useTableEditorTableStateSnapshot } from '@/state/table-editor-table'
- export interface GridHeaderActionsProps {
- table: Entity
- isRefetching: boolean
- }
- export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProps) => {
- const track = useTrack()
- const { ref } = useParams()
- const appSnap = useAppStateSnapshot()
- const snap = useTableEditorTableStateSnapshot()
- const { data: project } = useSelectedProjectQuery()
- const { data: org } = useSelectedOrganizationQuery()
- const { mutate: sendEvent } = useSendEventMutation()
- const [rlsConfirmModalOpen, setRlsConfirmModalOpen] = useState(false)
- const [realtimeDialogOpen, setRealtimeDialogOpen] = useState(false)
- const [indexAdvisorDialogOpen, setIndexAdvisorDialogOpen] = useState(false)
- const [isAutofixViewSecurityModalOpen, setIsAutofixViewSecurityModalOpen] = useState(false)
- const [showWarning, setShowWarning] = useQueryState(
- 'showWarning',
- parseAsBoolean.withDefault(false)
- )
- // need project lints to get security status for views
- const { data: lints = [] } = useProjectLintsQuery({ projectRef: project?.ref })
- // Use table-specific index advisor context
- const { isAvailable: isIndexAdvisorAvailable, isEnabled: isIndexAdvisorEnabled } =
- useTableIndexAdvisor()
- const isTable = isTableLike(table)
- const isForeignTable = isTableLikeForeignTable(table)
- const isView = isTableLikeView(table)
- const isMaterializedView = isTableLikeMaterializedView(table)
- const { realtimeAll: realtimeEnabled } = useIsFeatureEnabled(['realtime:all'])
- const { isSchemaLocked } = useIsProtectedSchema({ schema: table.schema })
- const isRealtimeEnabled = useIsTableRealtimeEnabled({ id: table.id })
- const { mutate: updateTable, isPending: isUpdatingTable } = useTableUpdateMutation({
- onError: (error) => {
- toast.error(`Failed to toggle RLS: ${error.message}`)
- },
- onSettled: () => {
- closeConfirmModal()
- },
- })
- const showHeaderActions = snap.selectedRows.size === 0
- 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 { can: canSqlWriteTables, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
- PermissionAction.TENANT_SQL_ADMIN_WRITE,
- 'tables'
- )
- const { can: canSqlWriteColumns } = useAsyncCheckPermissions(
- PermissionAction.TENANT_SQL_ADMIN_WRITE,
- 'columns'
- )
- const isReadOnly = !isLoadingPermissions && !canSqlWriteTables && !canSqlWriteColumns
- // This will change when we allow autogenerated API docs for schemas other than `public`
- const doesHaveAutoGeneratedAPIDocs = table.schema === 'public'
- const { hasLint: tableHasLints } = getEntityLintDetails(
- table.name,
- 'rls_disabled_in_public',
- ['ERROR'],
- lints,
- table.schema
- )
- const { hasLint: viewHasLints, matchingLint: matchingViewLint } = getEntityLintDetails(
- table.name,
- 'security_definer_view',
- ['ERROR', 'WARN'],
- lints,
- table.schema
- )
- const { hasLint: materializedViewHasLints, matchingLint: matchingMaterializedViewLint } =
- getEntityLintDetails(
- table.name,
- 'materialized_view_in_api',
- ['ERROR', 'WARN'],
- lints,
- table.schema
- )
- const closeConfirmModal = () => {
- setRlsConfirmModalOpen(false)
- }
- const onViewAPIDocs = () => {
- appSnap.setActiveDocsSection(['entities', table.name])
- appSnap.setShowProjectApiDocs(true)
- sendEvent({
- action: 'api_docs_opened',
- properties: {
- source: 'table_editor',
- },
- groups: {
- project: ref ?? 'Unknown',
- organization: org?.slug ?? 'Unknown',
- },
- })
- }
- const onToggleRLS = async () => {
- const payload = {
- id: table.id,
- rls_enabled: !(isTable && table.rls_enabled),
- }
- updateTable({
- projectRef: project?.ref!,
- connectionString: project?.connectionString,
- id: table.id,
- name: table.name,
- schema: table.schema,
- payload: payload,
- })
- track('table_rls_enabled', {
- method: 'table_editor',
- schema_name: table.schema,
- table_name: table.name,
- })
- }
- return (
- <div className="sb-grid-header__inner">
- {showHeaderActions && (
- <div className="flex items-center gap-x-2">
- {isReadOnly && (
- <Tooltip>
- <TooltipTrigger asChild>
- <div className="border border-strong rounded-sm bg-overlay-hover px-3 py-1 text-xs">
- Viewing as read-only
- </div>
- </TooltipTrigger>
- <TooltipContent side="bottom">
- You need additional permissions to manage your project's data
- </TooltipContent>
- </Tooltip>
- )}
- {isTable && !isSchemaLocked ? (
- table.rls_enabled ? (
- <>
- {policies.length < 1 && !isSchemaLocked ? (
- <ButtonTooltip
- asChild
- type="default"
- className="group"
- icon={<PlusCircle strokeWidth={1.5} className="text-foreground-muted" />}
- tooltip={{
- content: {
- side: 'bottom',
- className: 'w-[280px]',
- text: 'RLS is enabled for this table, but no policies are set. Select queries may return 0 results.',
- },
- }}
- >
- <Link passHref href={getTablePoliciesUrl(projectRef, table.schema, table.name)}>
- Add RLS policy
- </Link>
- </ButtonTooltip>
- ) : (
- <Button
- asChild
- type={policies.length < 1 && !isSchemaLocked ? 'warning' : 'default'}
- className="group"
- icon={
- isSchemaLocked || policies.length > 0 ? (
- <div
- className={cn(
- 'flex items-center justify-center rounded-full bg-border-stronger h-[16px]',
- policies.length > 9 ? ' px-1' : 'w-[16px]',
- ''
- )}
- >
- <span className="text-[11px] text-foreground font-mono text-center">
- {policies.length}
- </span>
- </div>
- ) : (
- <PlusCircle strokeWidth={1.5} />
- )
- }
- >
- <Link passHref href={getTablePoliciesUrl(projectRef, table.schema, table.name)}>
- RLS {policies.length > 1 ? 'policies' : 'policy'}
- </Link>
- </Button>
- )}
- </>
- ) : tableHasLints ? (
- <Popover modal={false} open={showWarning} onOpenChange={setShowWarning}>
- <PopoverTrigger asChild>
- <Button type="danger" icon={<Lock strokeWidth={1.5} />}>
- RLS disabled
- </Button>
- </PopoverTrigger>
- <PopoverContent className="w-80 text-sm" align="end">
- <h4 className="flex items-center gap-2">
- <Lock size={16} /> Row Level Security (RLS)
- </h4>
- <div className="grid gap-2 mt-4 text-foreground-light text-xs">
- <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>
- {!isSchemaLocked && (
- <Button
- type="default"
- className="mt-2 w-min"
- onClick={() => setRlsConfirmModalOpen(!rlsConfirmModalOpen)}
- >
- Enable RLS for this table
- </Button>
- )}
- </div>
- </PopoverContent>
- </Popover>
- ) : null
- ) : null}
- {isView && viewHasLints && (
- <SecurityDefinerViewPopover
- lint={matchingViewLint}
- onAutofix={() => {
- setIsAutofixViewSecurityModalOpen(true)
- }}
- />
- )}
- {isMaterializedView && materializedViewHasLints && (
- <SecurityDefinerViewPopover lint={matchingMaterializedViewLint} />
- )}
- {isForeignTable && table.schema === 'public' && (
- <Popover modal={false} open={showWarning} onOpenChange={setShowWarning}>
- <PopoverTrigger asChild>
- <Button type="warning" icon={<Unlock strokeWidth={1.5} />}>
- Unprotected Data API access
- </Button>
- </PopoverTrigger>
- <PopoverContent className="min-w-[395px] text-sm" align="end">
- <h3 className="flex items-center gap-2">
- <Unlock size={16} /> Secure Foreign table
- </h3>
- <div className="grid gap-2 mt-4 text-foreground-light text-sm">
- <p>
- Foreign tables do not enforce RLS, which may allow unrestricted access. To
- secure them, either move foreign tables to a private schema not exposed by
- PostgREST, or <a href="">disable PostgREST access</a> entirely.
- </p>
- <div className="mt-2">
- <Button type="default" asChild>
- <Link
- target="_blank"
- href={`${DOCS_URL}/guides/database/extensions/wrappers/overview#security`}
- >
- Learn more
- </Link>
- </Button>
- </div>
- </div>
- </PopoverContent>
- </Popover>
- )}
- <RoleImpersonationPopover header="View data as a role" align="center" />
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- type="default"
- icon={<MoreVertical />}
- className="h-7 w-7"
- aria-label="More actions"
- />
- </DropdownMenuTrigger>
- <DropdownMenuContent className="w-48">
- {isTable && realtimeEnabled && (
- <DropdownMenuItem className="gap-x-2" onClick={() => setRealtimeDialogOpen(true)}>
- <Realtime size={14} className={isRealtimeEnabled ? 'text-brand' : ''} />
- <span>{isRealtimeEnabled ? 'Disable' : 'Enable'} Realtime</span>
- </DropdownMenuItem>
- )}
- {doesHaveAutoGeneratedAPIDocs && (
- <DropdownMenuItem className="gap-x-2" onClick={() => onViewAPIDocs()}>
- <BookOpenText size={14} />
- <span>View API docs</span>
- </DropdownMenuItem>
- )}
- {isTable && isIndexAdvisorAvailable && !isIndexAdvisorEnabled && (
- <>
- <DropdownMenuSeparator />
- <DropdownMenuItem
- className="gap-x-2"
- onClick={() => setIndexAdvisorDialogOpen(true)}
- >
- <Lightbulb size={14} />
- <span>Enable Index Advisor</span>
- </DropdownMenuItem>
- </>
- )}
- </DropdownMenuContent>
- </DropdownMenu>
- <RefreshButton tableId={table.id} isRefetching={isRefetching} />
- {showHeaderActions && <InsertButton />}
- </div>
- )}
- <ViewEntityAutofixSecurityModal
- table={table}
- isAutofixViewSecurityModalOpen={isAutofixViewSecurityModalOpen}
- setIsAutofixViewSecurityModalOpen={setIsAutofixViewSecurityModalOpen}
- />
- {isTable && (
- <ConfirmationModal
- visible={rlsConfirmModalOpen}
- variant={table.rls_enabled ? 'destructive' : 'default'}
- title={`${table.rls_enabled ? 'Disable' : 'Enable'} Row Level Security`}
- description={`Are you sure you want to ${
- table.rls_enabled ? 'disable' : 'enable'
- } Row Level Security for this table?`}
- confirmLabel={`${table.rls_enabled ? 'Disable' : 'Enable'} RLS`}
- confirmLabelLoading={`${table.rls_enabled ? 'Disabling' : 'Enabling'} RLS`}
- loading={isUpdatingTable}
- onCancel={closeConfirmModal}
- onConfirm={onToggleRLS}
- />
- )}
- <RealtimeToggleDialog
- table={table}
- open={realtimeDialogOpen}
- setOpen={setRealtimeDialogOpen}
- />
- <EnableIndexAdvisorDialog open={indexAdvisorDialogOpen} setOpen={setIndexAdvisorDialogOpen} />
- </div>
- )
- }
|