| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369 |
- import {
- ident,
- joinSqlFragments,
- safeSql,
- type SafeSqlFragment,
- } from '@supabase/pg-meta/src/pg-format'
- import { PermissionAction } from '@supabase/shared-types/out/constants'
- import { Check, ChevronsUpDown } from 'lucide-react'
- import { useEffect, useState } from 'react'
- import { UseFormReturn } from 'react-hook-form'
- import {
- Button,
- cn,
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
- Input,
- Popover,
- PopoverContent,
- PopoverTrigger,
- RadioGroup,
- RadioGroupLargeItem,
- ScrollArea,
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectTrigger,
- } from 'ui'
- import {
- MultiSelector,
- MultiSelectorContent,
- MultiSelectorItem,
- MultiSelectorList,
- MultiSelectorTrigger,
- } from 'ui-patterns/multi-select'
- import { useDatabaseRolesQuery } from '@/data/database-roles/database-roles-query'
- import { useTablesQuery } from '@/data/tables/tables-query'
- import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
- import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
- interface PolicyDetailsV2Props {
- schema: string
- searchString?: string
- selectedTable?: string
- isEditing: boolean
- form: UseFormReturn<{
- name: string
- table: string
- behavior: string
- command: string
- roles: string
- }>
- onUpdateCommand: (command: string) => void
- onRolesChange?: (fragment: SafeSqlFragment) => void
- authContext: 'database' | 'realtime'
- }
- export const PolicyDetailsV2 = ({
- schema,
- searchString,
- selectedTable,
- isEditing,
- form,
- onUpdateCommand,
- onRolesChange,
- authContext,
- }: PolicyDetailsV2Props) => {
- const { data: project } = useSelectedProjectQuery()
- const [open, setOpen] = useState(false)
- const { can: canUpdatePolicies } = useAsyncCheckPermissions(
- PermissionAction.TENANT_SQL_ADMIN_WRITE,
- 'tables'
- )
- const { data: tables, isSuccess: isSuccessTables } = useTablesQuery({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- schema: schema,
- sortByProperty: 'name',
- includeColumns: true,
- })
- const { data: dbRoles } = useDatabaseRolesQuery({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- })
- const formattedRoles = (dbRoles ?? [])
- .map((role) => {
- return {
- id: role.id,
- name: role.name,
- value: role.name,
- disabled: false,
- }
- })
- .sort((a, b) => a.name.localeCompare(b.name))
- useEffect(() => {
- if (!isEditing && selectedTable === undefined) {
- const table = tables?.find(
- (table) =>
- table.schema === schema &&
- (table.id.toString() === searchString || table.name === searchString)
- )
- if (table) {
- form.setValue('table', table.name)
- } else if (isSuccessTables && tables.length > 0) {
- form.setValue('table', tables[0].name)
- }
- }
- }, [isEditing, form, searchString, tables, isSuccessTables, selectedTable])
- return (
- <>
- <div className="px-5 py-5 flex flex-col gap-y-4 border-b">
- <div className="items-start justify-between gap-4 grid grid-cols-12">
- <FormField
- control={form.control}
- name="name"
- render={({ field }) => (
- <FormItem className="col-span-6 flex flex-col gap-y-1">
- <FormLabel>Policy Name</FormLabel>
- <FormControl>
- <Input
- {...field}
- disabled={!canUpdatePolicies}
- className="bg-control border-control"
- placeholder="Provide a name for your policy"
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="table"
- render={({ field }) => (
- <FormItem className="col-span-6 flex flex-col gap-y-1">
- <FormLabel>
- Table
- <code className="text-code-inline">on</code> clause
- </FormLabel>
- {authContext === 'database' && (
- <FormControl>
- <Popover open={open} onOpenChange={setOpen} modal={false}>
- <PopoverTrigger asChild>
- <Button
- type="default"
- disabled={!canUpdatePolicies}
- className="w-full [&>span]:w-full h-[38px] text-sm"
- iconRight={
- <ChevronsUpDown
- className="text-foreground-muted"
- strokeWidth={2}
- size={14}
- />
- }
- >
- <div className="w-full flex gap-1">
- <span className="text-foreground">
- {schema}.{field.value}
- </span>
- </div>
- </Button>
- </PopoverTrigger>
- <PopoverContent
- className="p-0"
- side="bottom"
- align="start"
- sameWidthAsTrigger
- >
- <Command>
- <CommandInput placeholder="Find a table..." />
- <CommandList onWheel={(event) => event.stopPropagation()}>
- <CommandEmpty>No tables found</CommandEmpty>
- <CommandGroup>
- <ScrollArea className={(tables ?? []).length > 7 ? 'h-[200px]' : ''}>
- {(tables ?? []).map((table) => (
- <CommandItem
- key={table.id}
- className="cursor-pointer flex items-center justify-between space-x-2 w-full"
- onSelect={() => {
- form.setValue('table', table.name)
- setOpen(false)
- }}
- onClick={() => {
- form.setValue('table', table.name)
- setOpen(false)
- }}
- >
- <span className="flex items-center gap-1.5">
- {field.value === table.name ? <Check size={13} /> : ''}
- {table.name}
- </span>
- </CommandItem>
- ))}
- </ScrollArea>
- </CommandGroup>
- </CommandList>
- </Command>
- </PopoverContent>
- </Popover>
- </FormControl>
- )}
- {authContext === 'realtime' && (
- <FormControl>
- <Input
- disabled
- value="messages.realtime"
- className="bg-control border-control"
- />
- </FormControl>
- )}
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="behavior"
- render={({ field }) => (
- <FormItem className="col-span-6 flex flex-col gap-y-1">
- <FormLabel>
- Policy Behavior <code className="text-code-inline">as</code> clause
- </FormLabel>
- <FormControl>
- <Select
- disabled={isEditing}
- value={field.value}
- onValueChange={(value) => form.setValue('behavior', value)}
- >
- <SelectTrigger className="text-sm h-10 capitalize">{field.value}</SelectTrigger>
- <SelectContent>
- <SelectGroup>
- <SelectItem value="permissive" className="text-sm">
- <p>Permissive</p>
- <p className="text-foreground-light text-xs">
- Policies are combined using the "OR" Boolean operator
- </p>
- </SelectItem>
- <SelectItem value="restrictive" className="text-sm">
- <p>Restrictive</p>
- <p className="text-foreground-light text-xs">
- Policies are combined using the "AND" Boolean operator
- </p>
- </SelectItem>
- </SelectGroup>
- </SelectContent>
- </Select>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="command"
- render={({ field }) => (
- <FormItem className="col-span-12 flex flex-col gap-y-1">
- <FormLabel>
- Policy Command <code className="text-code-inline">for</code> clause
- </FormLabel>
- <FormControl>
- <RadioGroup
- disabled={isEditing}
- value={field.value}
- defaultValue={field.value}
- onValueChange={(value) => {
- form.setValue('command', value)
- onUpdateCommand(value)
- }}
- className={cn('flex flex-wrap gap-3', isEditing && 'opacity-50')}
- >
- {[
- 'select',
- 'insert',
- ...(authContext === 'database' ? ['update', 'delete', 'all'] : []),
- ].map((x) => (
- <RadioGroupLargeItem
- key={x}
- value={x}
- disabled={isEditing}
- label={x.toLocaleUpperCase()}
- className={cn('w-auto', isEditing && 'cursor-not-allowed')}
- />
- ))}
- </RadioGroup>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- <FormField
- control={form.control}
- name="roles"
- render={({ field }) => (
- <FormItem className="col-span-12 flex flex-col gap-y-1">
- <FormLabel htmlFor="roles">
- Target Roles <code className="text-code-inline">to</code> clause
- </FormLabel>
- <FormControl>
- <MultiSelector
- onValuesChange={(roles) => {
- field.onChange(roles.join(', '))
- onRolesChange?.(
- roles.length === 0
- ? safeSql`public`
- : joinSqlFragments(
- roles.map((r) => ident(r)),
- ', '
- )
- )
- }}
- disabled={!canUpdatePolicies}
- values={field.value.length === 0 ? [] : field.value?.split(', ')}
- size="small"
- >
- <MultiSelectorTrigger
- id="roles"
- mode="inline-combobox"
- label={
- field.value.length === 0
- ? 'Defaults to all (public) roles if none selected'
- : 'Search for a role'
- }
- badgeLimit="wrap"
- showIcon={false}
- deletableBadge
- className="w-full"
- />
- <MultiSelectorContent>
- <MultiSelectorList>
- {formattedRoles.map((role) => (
- <MultiSelectorItem
- key={role.id}
- value={role.value}
- disabled={role.disabled}
- >
- {role.name}
- </MultiSelectorItem>
- ))}
- </MultiSelectorList>
- </MultiSelectorContent>
- </MultiSelector>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- </div>
- </div>
- </>
- )
- }
|