mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
[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:
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_>
|
||||
)
|
||||
|
||||
@@ -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 }
|
||||
@@ -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)`,
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user