ruvector/studio/components/interfaces/TableGridEditor/GridHeaderActions.tsx
rUv 814f595995 feat(studio): Add complete RuVector Studio application
Major additions:
- Complete Next.js studio application with 1600+ components
- Docker support (Dockerfile.combined, docker-compose.yml)
- GCP deployment documentation and benchmarks
- SQL benchmark scripts for performance testing
- Sentry integration for monitoring
- Comprehensive test suite and mocks

Studio features:
- Dashboard and admin interfaces
- Data visualization components
- Authentication and user management
- API integration with RuVector backend
- Static data and public assets

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-06 23:04:48 +00:00

576 lines
22 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Lock, MousePointer2, PlusCircle, Unlock } from 'lucide-react'
import Link from 'next/link'
import { useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { RefreshButton } from 'components/grid/components/header/RefreshButton'
import { getEntityLintDetails } from 'components/interfaces/TableGridEditor/TableEntity.utils'
import { APIDocsButton } from 'components/ui/APIDocsButton'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
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 { useDatabaseTriggersQuery } from 'data/database-triggers/database-triggers-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 { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { RealtimeButtonVariant, useRealtimeExperiment } from 'hooks/misc/useRealtimeExperiment'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import { DOCS_URL } from 'lib/constants'
import { useTrack } from 'lib/telemetry/track'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { useTableEditorTableStateSnapshot } from 'state/table-editor-table'
import {
Button,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
Tooltip,
TooltipContent,
TooltipTrigger,
cn,
} from 'ui'
import ConfirmModal from 'ui-patterns/Dialogs/ConfirmDialog'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { RoleImpersonationPopover } from '../RoleImpersonationSelector/RoleImpersonationPopover'
import ViewEntityAutofixSecurityModal from './ViewEntityAutofixSecurityModal'
export interface GridHeaderActionsProps {
table: Entity
isRefetching: boolean
}
export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProps) => {
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()
const track = useTrack()
const [showWarning, setShowWarning] = useQueryState(
'showWarning',
parseAsBoolean.withDefault(false)
)
// need project lints to get security status for views
const { data: lints = [] } = useProjectLintsQuery({ projectRef: project?.ref })
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 { mutate: updateTable } = useTableUpdateMutation({
onError: (error) => {
toast.error(`Failed to toggle RLS: ${error.message}`)
},
onSettled: () => {
closeConfirmModal()
},
})
const [showEnableRealtime, setShowEnableRealtime] = useState(false)
const [rlsConfirmModalOpen, setRlsConfirmModalOpen] = useState(false)
const [isAutofixViewSecurityModalOpen, setIsAutofixViewSecurityModalOpen] = useState(false)
const snap = useTableEditorTableStateSnapshot()
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 { 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) => t.id === table?.id)
const { activeVariant: activeRealtimeVariant } = useRealtimeExperiment({
projectInsertedAt: project?.inserted_at,
isTable,
isRealtimeEnabled,
})
const { mutate: updatePublications, isPending: isTogglingRealtime } =
useDatabasePublicationUpdateMutation({
onSuccess: () => {
setShowEnableRealtime(false)
track(isRealtimeEnabled ? 'table_realtime_disabled' : 'table_realtime_enabled', {
method: 'ui',
schema_name: table.schema,
table_name: table.name,
})
},
onError: (error) => {
toast.error(`Failed to toggle realtime for ${table.name}: ${error.message}`)
},
})
const { data: triggersData } = useDatabaseTriggersQuery(
{
projectRef: project?.ref,
connectionString: project?.connectionString,
},
{
enabled: isTable,
}
)
const tableTriggers = (triggersData ?? []).filter(
(trigger) => trigger.schema === table.schema && trigger.table === table.name
)
const tableTriggersCount = tableTriggers.length
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: 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 manageTriggersHref = `/project/${ref}/database/triggers?schema=${table.schema}`
const toggleRealtime = async () => {
if (!project || !realtimePublication) return
const exists = realtimeEnabledTables.some((x) => x.id === table.id)
const tables = !exists
? [`${table.schema}.${table.name}`].concat(
realtimeEnabledTables.map((t) => `${t.schema}.${t.name}`)
)
: realtimeEnabledTables.filter((x) => x.id !== table.id).map((x) => `${x.schema}.${x.name}`)
track('realtime_toggle_table_clicked', {
newState: exists ? 'disabled' : 'enabled',
origin: 'tableGridHeader',
})
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: !(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 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={`/project/${projectRef}/auth/policies?search=${table.name}&schema=${table.schema}`}
>
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={`/project/${projectRef}/auth/policies?search=${table.name}&schema=${table.schema}`}
>
RLS {policies.length > 1 ? 'policies' : 'policy'}
</Link>
</Button>
)}
</>
) : (
<Popover_Shadcn_ modal={false} open={showWarning} onOpenChange={setShowWarning}>
<PopoverTrigger_Shadcn_ asChild>
<Button type="warning" icon={<Lock strokeWidth={1.5} />}>
RLS disabled
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_
// using `portal` for a safari fix. issue with rendering outside of body element
portal
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_Shadcn_>
</Popover_Shadcn_>
)
) : null}
{isTable && activeRealtimeVariant === RealtimeButtonVariant.TRIGGERS ? (
<Button
asChild
type={'default'}
size="tiny"
icon={
<div
className={cn(
'flex items-center justify-center rounded-full bg-border-stronger h-[16px]',
tableTriggersCount > 9 ? 'px-1' : 'w-[16px]'
)}
>
<span className="text-[11px] text-foreground font-mono text-center">
{tableTriggersCount}
</span>
</div>
}
>
<Link href={manageTriggersHref}>
{tableTriggersCount === 1 ? 'Trigger' : 'Triggers'}
</Link>
</Button>
) : (
activeRealtimeVariant !== RealtimeButtonVariant.HIDE_BUTTON &&
realtimeEnabled && (
<ButtonTooltip
type="default"
size="tiny"
icon={
<MousePointer2
strokeWidth={1.5}
className={isRealtimeEnabled ? 'text-brand' : 'text-foreground-muted'}
/>
}
onClick={() => setShowEnableRealtime(true)}
className={cn(isRealtimeEnabled && 'w-7 h-7 p-0 text-brand hover:text-brand-hover')}
tooltip={{
content: {
side: 'bottom',
text: isRealtimeEnabled
? 'Click to disable realtime for this table'
: 'Click to enable realtime for this table',
},
}}
>
{!isRealtimeEnabled && 'Enable Realtime'}
</ButtonTooltip>
)
)}
{isView && viewHasLints && (
<Popover_Shadcn_ modal={false} open={showWarning} onOpenChange={setShowWarning}>
<PopoverTrigger_Shadcn_ asChild>
<Button type="warning" icon={<Unlock strokeWidth={1.5} />}>
Security Definer view
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_
// using `portal` for a safari fix. issue with rendering outside of body element
portal
className="min-w-[395px] text-sm"
align="end"
>
<h3 className="flex items-center gap-2">
<Unlock size={16} /> Secure your View
</h3>
<div className="grid gap-2 mt-4 text-foreground-light text-sm">
<p>
This view is defined with the Security Definer property, giving it permissions
of the view's creator (Postgres), rather than the permissions of the querying
user.
</p>
<p>
Since this view is in the public schema, it is accessible via your project's
APIs.
</p>
<div className="mt-2 flex items-center gap-2">
<Button
type="secondary"
onClick={() => {
setIsAutofixViewSecurityModalOpen(true)
}}
>
Autofix
</Button>
<Button type="default" asChild>
<Link
target="_blank"
href={`/project/${ref}/advisors/security?preset=${matchingViewLint?.level}&id=${matchingViewLint?.cache_key}`}
>
Learn more
</Link>
</Button>
</div>
</div>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
)}
{isMaterializedView && materializedViewHasLints && (
<Popover_Shadcn_ modal={false} open={showWarning} onOpenChange={setShowWarning}>
<PopoverTrigger_Shadcn_ asChild>
<Button type="warning" icon={<Unlock strokeWidth={1.5} />}>
Security Definer view
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_
// using `portal` for a safari fix. issue with rendering outside of body element
portal
className="min-w-[395px] text-sm"
align="end"
>
<h3 className="flex items-center gap-2">
<Unlock size={16} /> Secure your View
</h3>
<div className="grid gap-2 mt-4 text-foreground-light text-sm">
<p>
This view is defined with the Security Definer property, giving it permissions
of the view's creator (Postgres), rather than the permissions of the querying
user.
</p>
<p>
Since this view is in the public schema, it is accessible via your project's
APIs.
</p>
<div className="mt-2">
<Button type="default" asChild>
<Link
target="_blank"
href={`/project/${ref}/advisors/security?preset=${matchingMaterializedViewLint?.level}&id=${matchingMaterializedViewLint?.cache_key}`}
>
Learn more
</Link>
</Button>
</div>
</div>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
)}
{isForeignTable && table.schema === 'public' && (
<Popover_Shadcn_ modal={false} open={showWarning} onOpenChange={setShowWarning}>
<PopoverTrigger_Shadcn_ asChild>
<Button type="warning" icon={<Unlock strokeWidth={1.5} />}>
Unprotected Data API access
</Button>
</PopoverTrigger_Shadcn_>
<PopoverContent_Shadcn_
// using `portal` for a safari fix. issue with rendering outside of body element
portal
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_Shadcn_>
</Popover_Shadcn_>
)}
<RoleImpersonationPopover serviceRoleLabel="postgres" />
{doesHaveAutoGeneratedAPIDocs && (
<APIDocsButton section={['entities', table.name]} source="table_editor" />
)}
<RefreshButton tableId={table.id} isRefetching={isRefetching} />
</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>
<ViewEntityAutofixSecurityModal
table={table}
isAutofixViewSecurityModalOpen={isAutofixViewSecurityModalOpen}
setIsAutofixViewSecurityModalOpen={setIsAutofixViewSecurityModalOpen}
/>
{isTable && (
<ConfirmModal
danger={table.rls_enabled}
visible={rlsConfirmModalOpen}
title="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}
/>
)}
</div>
)
}