[Dashboard] Data API on/off switch (#26796)

* init layouts in project settings

* Update general.tsx

* update gap

* Update Scaffold.tsx

* Update PostgrestConfig.tsx

* Update PostgrestConfig.tsx

* spacing issues

* now added a enabled switch

* Revert "now added a enabled switch"

This reverts commit f22050302a.

* Update PostgrestConfig.tsx

* Update PostgrestConfig.tsx

* revert

* Update project-postgrest-config-update-mutation.ts

* add bottom padding

* Update PostgrestConfig.tsx

* Update PostgrestConfig.tsx

* fix

* Update PostgrestConfig.tsx

* Update PostgrestConfig.tsx

* Update PostgrestConfig.tsx

* Update PostgrestConfig.tsx

* Update PostgrestConfig.tsx

* add collapible

* update FormPanel component to be more composable

* Update PostgrestConfig.tsx

* Update PostgrestConfig.tsx

* update callout to warning variant

* Update FormLayout.tsx

* Update PostgrestConfig.tsx

* Small copy changes

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
Jonathan Summers-MuirandJoshen Lim authored and GitHub committed 2024-06-04 16:02:47 +07:00
1 parent 9e45053d25
commit 79c5bb9788
7 files changed
+436 -234

No files matched your search

@@ -100,7 +100,14 @@ export const OngoingQueriesPanel = ({ visible, onClose }: OngoingQueriesPanel) =
)}
{queries.length === 0 && (
<div className="flex flex-col gap-y-2 items-center justify-center h-full text-foreground-light text-sm">
<span>No ongoing queries running on database</span>
<span>
No queries are currently running on the{' '}
{database?.identifier !== project?.ref
? `read replica ${database?.identifier}`
: (databases ?? []).length > 1
? 'primary database'
: 'database'}
</span>
<Button
type="default"
loading={isLoadingOngoingQueries || isFetchingOngoingQueries}
@@ -2,9 +2,16 @@ import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { FormActions, FormPanel } from 'components/ui/Forms'
import {
FormActions,
FormPanelContainer,
FormPanelContent,
FormPanelFooter,
FormPanelHeader,
} from 'components/ui/Forms'
import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query'
import { useProjectPostgrestConfigUpdateMutation } from 'data/config/project-postgrest-config-update-mutation'
import { useDatabaseExtensionDisableMutation } from 'data/database-extensions/database-extension-disable-mutation'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { useSchemasQuery } from 'data/database/schemas-query'
import { useCheckPermissions } from 'hooks'
@@ -15,14 +22,23 @@ import { useForm } from 'react-hook-form'
import toast from 'react-hot-toast'
import {
Admonition,
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
Button,
CollapsibleContent_Shadcn_,
Collapsible_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
FormItem_Shadcn_,
Form_Shadcn_,
Input_Shadcn_,
Separator,
Skeleton,
Switch,
} from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { WarningIcon } from 'ui-patterns/Icons/StatusIcons'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import {
MultiSelector,
@@ -34,29 +50,45 @@ import {
} from 'ui-patterns/multi-select'
import { z } from 'zod'
const formSchema = z.object({
dbSchema: z.array(z.string()),
dbExtraSearchPath: z.string(),
maxRows: z.number().max(1000000, "Can't be more than 1,000,000"),
dbPool: z
.number()
.min(0, 'Must be more than 0')
.max(1000, "Can't be more than 1000")
.optional()
.nullable(),
})
const formSchema = z
.object({
dbSchema: z.array(z.string()),
dbExtraSearchPath: z.string(),
maxRows: z.number().max(1000000, "Can't be more than 1,000,000"),
dbPool: z
.number()
.min(0, 'Must be more than 0')
.max(1000, "Can't be more than 1000")
.optional()
.nullable(),
enableDataApi: z.boolean(),
})
.refine(
(data) => {
if (data.enableDataApi && data.dbSchema.length === 0) {
return false
}
return true
},
{
message: 'Must have at least one schema if Data API is enabled',
path: ['dbSchema'],
}
)
const PostgrestConfig = () => {
const { ref: projectRef } = useParams()
const { project } = useProjectContext()
const { data: config, isError } = useProjectPostgrestConfigQuery({ projectRef })
const { data: extensions } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { data: schemas, isLoading: isLoadingSchemas } = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const { data: config, isError } = useProjectPostgrestConfigQuery({ projectRef })
const { mutate: updatePostgrestConfig, isLoading: isUpdating } =
useProjectPostgrestConfigUpdateMutation({
onSuccess: () => {
@@ -64,15 +96,14 @@ const PostgrestConfig = () => {
},
})
const formId = 'project-postgres-config'
const hiddenSchema = ['auth', 'pgbouncer', 'hooks', 'extensions']
const canUpdatePostgrestConfig = useCheckPermissions(
PermissionAction.UPDATE,
'custom_config_postgrest'
)
const { data: extensions } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const isGraphqlExtensionEnabled =
(extensions ?? []).find((ext) => ext.name === 'pg_graphql')?.installed_version !== null
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
@@ -85,21 +116,6 @@ const PostgrestConfig = () => {
},
})
const formId = 'project-postgres-config'
const onSubmit = async (values: z.infer<typeof formSchema>) => {
if (!projectRef) return console.error('Project ref is required') // is this needed ?
updatePostgrestConfig({
projectRef,
dbSchema: values.dbSchema.join(', '),
maxRows: values.maxRows,
dbExtraSearchPath: values.dbExtraSearchPath,
dbPool: values.dbPool ? values.dbPool : null,
})
}
const hiddenSchema = ['auth', 'pgbouncer', 'hooks', 'extensions']
const schema =
schemas
?.filter((x) => {
@@ -115,34 +131,301 @@ const PostgrestConfig = () => {
}
}) ?? []
const isGraphqlExtensionEnabled =
(extensions ?? []).find((ext) => ext.name === 'pg_graphql')?.installed_version !== null
function resetForm(values?: Partial<z.infer<typeof formSchema>>) {
const dbSchema =
config?.db_schema && config?.db_schema ? config.db_schema.replace(/ /g, '').split(',') : []
if (values?.enableDataApi && dbSchema.length === 0) {
dbSchema.push('public', 'storage')
}
const defaultValues = {
dbSchema: values?.dbSchema ?? dbSchema,
maxRows: values?.maxRows ?? config?.max_rows,
dbExtraSearchPath: values?.dbExtraSearchPath ?? config?.db_extra_search_path,
dbPool: values?.dbPool ?? config?.db_pool,
enableDataApi: values?.enableDataApi,
}
function resetForm() {
if (config) {
form.reset({
dbSchema: config.db_schema ? config.db_schema.replace(/ /g, '').split(',') : [],
maxRows: config.max_rows,
dbExtraSearchPath: config.db_extra_search_path,
dbPool: config.db_pool,
...defaultValues,
})
}
}
const onSubmit = async (values: z.infer<typeof formSchema>) => {
if (!projectRef) return console.error('Project ref is required') // is this needed ?
updatePostgrestConfig({
projectRef,
dbSchema: values.dbSchema.join(', '),
maxRows: values.maxRows,
dbExtraSearchPath: values.dbExtraSearchPath,
dbPool: values.dbPool ? values.dbPool : null,
})
}
useEffect(() => {
if (schema && config) {
resetForm()
if (config) {
/**
* Checks if enableDataApi should be enabled or disabled
* based on the db_schema value being empty string
*/
const enableDataApi = config.db_schema ? true : false
/**
* Reset the form to default values
*/
resetForm({ enableDataApi })
}
}, [schema && config])
// ignore dep array warning, as cant have resetForm as a dependency
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [config])
const isDataApiEnabledInForm = form.getValues('enableDataApi')
return (
<Form_Shadcn_ {...form}>
<form id={formId} onSubmit={form.handleSubmit(onSubmit)}>
<FormPanel
disabled={true}
header={<p>Data API Settings</p>}
footer={
<div className="flex px-8 py-4">
<FormPanelContainer>
<FormPanelHeader>Data API Settings</FormPanelHeader>
<FormPanelContent>
{isError ? (
<Admonition type="destructive" title="Failed to retrieve API settings" />
) : (
<>
<FormField_Shadcn_
control={form.control}
name="enableDataApi"
render={({ field }) => (
<FormItem_Shadcn_ className="w-full">
<FormItemLayout
className="w-full px-8 py-8"
layout="flex"
label="Enable Data API"
description="When enabled you will be able to use any Supabase client library and PostgREST endpoints with any schema configured below."
>
<FormControl_Shadcn_>
<Switch
size="large"
checked={field.value}
onCheckedChange={(value) => {
field.onChange(value)
if (!value) {
/**
* reset the form to default values
*/
resetForm({ enableDataApi: false, dbSchema: [] })
/**
* remove all the schema values when disabling the Data API
*/
} else {
/**
* reset the form to default values
* when disabled the Data API
*/
resetForm({ enableDataApi: true })
}
}}
/>
</FormControl_Shadcn_>
</FormItemLayout>
{!field.value && (
<>
<Separator />
<Alert_Shadcn_
variant="warning"
className="mb-0 border-none rounded-none"
>
<WarningIcon className="!left-[2rem]" />
<AlertTitle_Shadcn_ className="!pl-[3.5rem] !left-[6rem]">
No schemas can be queried
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ className="!pl-[3.5rem]">
<p>
With this setting disabled, you will not be able to query any
schemas via the Data API.
</p>
<p>
You will see errors from the Postgrest endpoint
<code className="text-xs">/rest/v1/</code>.
</p>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
</>
)}
</FormItem_Shadcn_>
)}
/>
<Collapsible_Shadcn_ open={form.getValues('enableDataApi')}>
<CollapsibleContent_Shadcn_ className="border-t divide-y transition-all data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down">
<FormField_Shadcn_
control={form.control}
name="dbSchema"
render={({ field }) => (
<FormItem_Shadcn_ className="w-full">
<FormItemLayout
label="Exposed schemas"
description="The schemas to expose in your API. Tables, views and stored procedures in
these schemas will get API endpoints."
layout="horizontal"
className="px-8 py-8"
>
{isLoadingSchemas ? (
<div className="col-span-12 flex flex-col gap-2 lg:col-span-7">
<Skeleton className="w-full h-[38px]" />
</div>
) : (
<MultiSelector
onValuesChange={field.onChange}
values={field.value}
size={'small'}
disabled={!canUpdatePostgrestConfig || !isDataApiEnabledInForm}
>
<MultiSelectorTrigger>
<MultiSelectorInput placeholder="Select schemas for Data API..." />
</MultiSelectorTrigger>
<MultiSelectorContent>
{schema.length <= 0 ? (
<MultiSelectorList>
<MultiSelectorItem key={'empty'} value={'no'}>
no
</MultiSelectorItem>
</MultiSelectorList>
) : (
<MultiSelectorList>
{schema.map((x, i) => (
<MultiSelectorItem key={x.id + '-' + i} value={x.name}>
{x.name}
</MultiSelectorItem>
))}
</MultiSelectorList>
)}
</MultiSelectorContent>
</MultiSelector>
)}
{!field.value.includes('public') && field.value.length > 0 && (
<Admonition
type="default"
title="The public schema for this project is not exposed"
className="mt-2"
description={
<>
<p>
You will not be able to query tables and views in the{' '}
<code>public</code> schema via supabase-js or HTTP clients.
</p>
{isGraphqlExtensionEnabled && (
<>
<p>
Tables in the <code className="text-xs">public</code>{' '}
schema are still exposed over our GraphQL endpoints.
</p>
<Button asChild type="default">
<Link href={`/project/${projectRef}/database/extensions`}>
Disable the pg_graphql extension
</Link>
</Button>
</>
)}
</>
}
/>
)}
</FormItemLayout>
</FormItem_Shadcn_>
)}
/>
<FormField_Shadcn_
control={form.control}
name="dbExtraSearchPath"
render={({ field }) => (
<FormItem_Shadcn_ className="w-full">
<FormItemLayout
className="w-full px-8 py-8"
layout="horizontal"
label="Extra search path"
description="Extra schemas to add to the search path of every request. Multiple schemas must be comma-separated."
>
<FormControl_Shadcn_>
<Input_Shadcn_
size="small"
disabled={!canUpdatePostgrestConfig || !isDataApiEnabledInForm}
{...field}
/>
</FormControl_Shadcn_>
</FormItemLayout>
</FormItem_Shadcn_>
)}
/>
<FormField_Shadcn_
control={form.control}
name="maxRows"
render={({ field }) => (
<FormItem_Shadcn_ className="w-full">
<FormItemLayout
className="w-full px-8 py-8"
layout="horizontal"
label="Max rows"
description="The maximum number of rows returned from a view, table, or stored procedure. Limits payload size for accidental or malicious requests."
>
<FormControl_Shadcn_>
<Input_Shadcn_
size="small"
disabled={!canUpdatePostgrestConfig || !isDataApiEnabledInForm}
{...field}
type="number"
{...form.register('maxRows', {
valueAsNumber: true, // Ensure the value is handled as a number
})}
/>
</FormControl_Shadcn_>
</FormItemLayout>
</FormItem_Shadcn_>
)}
/>
<FormField_Shadcn_
control={form.control}
name="dbPool"
render={({ field }) => (
<FormItem_Shadcn_ className="w-full">
<FormItemLayout
className="w-full px-8 py-8"
layout="horizontal"
label="Pool size"
description="Number of maximum connections to keep open in the Data API server's database pool. Unset to let it be configured automatically based on compute size."
>
<FormControl_Shadcn_>
<Input_Shadcn_
size="small"
disabled={!canUpdatePostgrestConfig || !isDataApiEnabledInForm}
{...field}
type="number"
placeholder="Configured automatically based on compute size"
onChange={(e) =>
field.onChange(
e.target.value === '' ? null : Number(e.target.value)
)
}
value={field.value === null ? '' : field.value}
/>
</FormControl_Shadcn_>
</FormItemLayout>
</FormItem_Shadcn_>
)}
/>
</CollapsibleContent_Shadcn_>
</Collapsible_Shadcn_>
</>
)}
<FormPanelFooter className="flex px-8 py-4">
<FormActions
form={formId}
isSubmitting={isUpdating}
@@ -155,166 +438,9 @@ const PostgrestConfig = () => {
: undefined
}
/>
</div>
}
>
{isError ? (
<Admonition type="destructive" title="Failed to retrieve API settings" />
) : (
<>
<FormField_Shadcn_
control={form.control}
name="dbSchema"
render={({ field }) => (
<FormItem_Shadcn_ className="w-full">
<FormItemLayout
label="Exposed schemas"
description="The schemas to expose in your API. Tables, views and stored procedures in
these schemas will get API endpoints."
layout="horizontal"
className="px-8 py-8"
>
{isLoadingSchemas ? (
<div className="col-span-12 flex flex-col gap-2 lg:col-span-7">
<Skeleton className="w-full h-[38px]" />
</div>
) : (
<MultiSelector
onValuesChange={field.onChange}
values={field.value}
size={'small'}
>
<MultiSelectorTrigger>
<MultiSelectorInput placeholder="Select schemas for Data API..." />
</MultiSelectorTrigger>
<MultiSelectorContent>
<MultiSelectorList>
{schema.map((x, i) => (
<MultiSelectorItem key={x.id + '-' + i} value={x.name}>
{x.name}
</MultiSelectorItem>
))}
</MultiSelectorList>
</MultiSelectorContent>
</MultiSelector>
)}
{!field.value.includes('public') && (
<Admonition
type="default"
title="The public schema for this project is not exposed"
className="mt-2"
>
<>
<p>
You will not be able to query tables and views in the{' '}
<code>public</code> schema via supabase-js or HTTP clients.
</p>
{isGraphqlExtensionEnabled && (
<>
<p>
Tables in the <code className="text-xs">public</code> schema are
still exposed over our GraphQL endpoints.
</p>
<Button asChild type="default">
<Link
href={`/project/${projectRef}/database/extensions`}
// className="!no-underline !hover:bg-surface-100 !text-foreground"
>
Disable the pg_graphql extension
</Link>
</Button>
</>
)}
</>
</Admonition>
)}
</FormItemLayout>
</FormItem_Shadcn_>
)}
/>
<FormField_Shadcn_
control={form.control}
name="dbExtraSearchPath"
render={({ field }) => (
<FormItem_Shadcn_ className="w-full">
<FormItemLayout
className="w-full px-8 py-8"
layout="horizontal"
label="Extra search path"
description="Extra schemas to add to the search path of every request. Multiple schemas must be comma-separated."
>
<FormControl_Shadcn_>
<Input_Shadcn_
size="small"
disabled={!canUpdatePostgrestConfig}
{...field}
/>
</FormControl_Shadcn_>
</FormItemLayout>
</FormItem_Shadcn_>
)}
/>
<FormField_Shadcn_
control={form.control}
name="maxRows"
render={({ field }) => (
<FormItem_Shadcn_ className="w-full">
<FormItemLayout
className="w-full px-8 py-8"
layout="horizontal"
label="Max rows"
description="The maximum number of rows returned from a view, table, or stored procedure. Limits payload size for accidental or malicious requests."
>
<FormControl_Shadcn_>
<Input_Shadcn_
size="small"
disabled={!canUpdatePostgrestConfig}
{...field}
type="number"
{...form.register('maxRows', {
valueAsNumber: true, // Ensure the value is handled as a number
})}
/>
</FormControl_Shadcn_>
</FormItemLayout>
</FormItem_Shadcn_>
)}
/>
<FormField_Shadcn_
control={form.control}
name="dbPool"
render={({ field }) => (
<FormItem_Shadcn_ className="w-full">
<FormItemLayout
className="w-full px-8 py-8"
layout="horizontal"
label="Pool size"
description="Number of maximum connections to keep open in the Data API server's database pool. Unset to let it be configured automatically based on compute size."
>
<FormControl_Shadcn_>
<Input_Shadcn_
size="small"
disabled={!canUpdatePostgrestConfig}
{...field}
type="number"
placeholder="Configured automatically based on compute size"
onChange={(e) =>
field.onChange(e.target.value === '' ? null : Number(e.target.value))
}
value={field.value === null ? '' : field.value}
/>
</FormControl_Shadcn_>
</FormItemLayout>
</FormItem_Shadcn_>
)}
/>
</>
)}
</FormPanel>
</FormPanelFooter>
</FormPanelContent>
</FormPanelContainer>
</form>
</Form_Shadcn_>
)
+53 -17
View File
@@ -1,3 +1,6 @@
import { HTMLAttributes, forwardRef } from 'react'
import { cn } from 'ui'
interface Props {
children: React.ReactNode
header?: React.ReactNode
@@ -9,22 +12,55 @@ interface Props {
}
const FormPanel = ({ children, header, footer }: Props) => (
<div
className="
bg-surface-100
border-default
overflow-hidden
rounded-md border shadow"
>
{header && <div className="border-default border-b px-8 py-4">{header}</div>}
<div className="divide-border flex flex-col gap-0 divide-y">{children}</div>
{footer && (
<>
<div className="border-default border-t"></div>
{footer}
</>
)}
</div>
<FormPanelContainer>
{header && <FormPanelHeader>{header}</FormPanelHeader>}
<FormPanelContent className="divide-y">{children}</FormPanelContent>
{footer && <FormPanelFooter>{footer}</FormPanelFooter>}
</FormPanelContainer>
)
export { FormPanel }
const FormPanelContainer = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ children, ...props }, ref) => (
<div
ref={ref}
{...props}
className={cn('bg-surface-100 border overflow-hidden rounded-md shadow', props.className)}
>
{children}
</div>
)
)
FormPanelContainer.displayName = FormPanelContainer.displayName
const FormPanelHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ children, ...props }, ref) => (
<div ref={ref} {...props} className={cn('border-default border-b px-8 py-4', props.className)}>
{children}
</div>
)
)
FormPanelHeader.displayName = FormPanelHeader.displayName
const FormPanelContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ children, ...props }, ref) => (
<div ref={ref} {...props} className={cn('divide-border flex flex-col gap-0', props.className)}>
{children}
</div>
)
)
FormPanelContent.displayName = FormPanelContent.displayName
const FormPanelFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
({ children, ...props }, ref) => (
<div ref={ref} {...props} className={cn('border-t', props.className)}>
{children}
</div>
)
)
FormPanelFooter.displayName = FormPanelFooter.displayName
export { FormPanel, FormPanelContainer, FormPanelContent, FormPanelHeader, FormPanelFooter }
+10
View File
@@ -380,12 +380,22 @@ const uiConfig = ui({
from: { height: 'var(--radix-accordion-content-height)' },
to: { height: 0 },
},
'collapsible-down': {
from: { height: 0 },
to: { height: 'var(--radix-collapsible-content-height)' },
},
'collapsible-up': {
from: { height: 'var(--radix-collapsible-content-height)' },
to: { height: 0 },
},
},
animation: {
'flash-code': 'flash-code 1s forwards',
'flash-code-slow': 'flash-code 2s forwards',
'accordion-down': 'accordion-down 0.15s ease-out',
'accordion-up': 'accordion-up 0.15s ease-out',
'collapsible-down': 'collapsible-down 0.10s ease-out',
'collapsible-up': 'collapsible-up 0.10s ease-out',
},
borderRadius: {
// lg: `var(--radius)`,
+19 -3
View File
@@ -17,6 +17,10 @@ export interface AdmonitionProps {
title?: string
description?: string | React.ReactNode
showIcon?: boolean
childProps?: {
title?: React.ComponentProps<typeof AlertTitle>
description?: React.ComponentProps<typeof AlertDescription>
}
}
const admonitionToAlertMapping: Record<
@@ -113,17 +117,29 @@ export const Admonition = forwardRef<
{label || title ? (
<>
<AlertTitle
{...props.childProps?.title}
className={cn(
'text mt-0.5 flex gap-3 text-sm [&_p]:mb-1.5 [&_p]:mt-0',
!label && 'flex-col'
!label && 'flex-col',
props.childProps?.title?.className
)}
>
{label || title}
</AlertTitle>
{description && <AlertDescription>{description}</AlertDescription>}
{description && (
<AlertDescription className={props.childProps?.description?.className}>
{description}
</AlertDescription>
)}
{/* // children is to handle Docs and MDX issues with children and <p> elements */}
{children && (
<AlertDescription className="mt-3 [&_p]:mb-1.5 [&_p]:mt-0">
<AlertDescription
{...props.childProps?.description}
className={cn(
'mt-3 [&_p]:mb-1.5 [&_p]:mt-0',
props.childProps?.description?.className
)}
>
{children}
</AlertDescription>
)}
@@ -26,7 +26,7 @@ type Props = {
name?: string
}
const ContainerVariants = cva('grid gap-2', {
const ContainerVariants = cva('relative grid gap-2', {
variants: {
size: {
tiny: 'text-xs',
@@ -43,6 +43,7 @@ type MultiSelectorProps = {
values: string[]
onValuesChange: (value: string[]) => void
loop?: boolean
disabled?: boolean
} & React.ComponentPropsWithoutRef<typeof Command> &
VariantProps<typeof MultiSelectorVariants>
@@ -56,6 +57,7 @@ interface MultiSelectContextProps {
activeIndex: number
setActiveIndex: React.Dispatch<React.SetStateAction<number>>
size: MultiSelectorProps['size']
disabled?: boolean
}
const MultiSelectContext = createContext<MultiSelectContextProps | null>(null)
@@ -76,6 +78,7 @@ const MultiSelector = ({
children,
dir,
size = 'small',
disabled,
...props
}: MultiSelectorProps) => {
const [inputValue, setInputValue] = useState('')
@@ -154,6 +157,7 @@ const MultiSelector = ({
activeIndex,
setActiveIndex,
size: size || 'small',
disabled,
}}
>
<CommandPrimitive
@@ -202,7 +206,7 @@ const MultiSelectorTriggerBadgeVariants = cva(
const MultiSelectorTrigger = forwardRef<HTMLDivElement, ButtonVariantProps>(
({ className, children, ...props }, ref) => {
const { value, onValueChange, activeIndex, size } = useMultiSelect()
const { value, onValueChange, activeIndex, size, disabled } = useMultiSelect()
const mousePreventDefault = useCallback((e: React.MouseEvent) => {
e.preventDefault()
@@ -214,6 +218,7 @@ const MultiSelectorTrigger = forwardRef<HTMLDivElement, ButtonVariantProps>(
ref={ref}
className={cn(
'flex flex-wrap gap-1 border border-control bg-foreground/[.026] rounded-lg px-1',
disabled && 'cursor-not-allowed opacity-50',
className
)}
{...props}
@@ -264,7 +269,8 @@ const MultiSelectorInput = forwardRef<
React.ElementRef<typeof CommandInput>,
React.ComponentPropsWithoutRef<typeof CommandInput>
>(({ className, ...props }, ref) => {
const { setOpen, inputValue, setInputValue, activeIndex, setActiveIndex, size } = useMultiSelect()
const { setOpen, inputValue, setInputValue, activeIndex, setActiveIndex, size, disabled } =
useMultiSelect()
return (
<CommandInput
{...props}
@@ -274,6 +280,7 @@ const MultiSelectorInput = forwardRef<
onBlur={() => setOpen(false)}
onFocus={() => setOpen(true)}
onClick={() => setActiveIndex(-1)}
disabled={disabled}
className={cn(
MultiSelectorInputVariants({ size }),
'text-sm bg-transparent border-none outline-none placeholder:text-foreground-muted flex-1',