mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
Fix wrapper forms design (#46283)
## Screenshots Before: <img width="983" height="959" alt="image" src="https://github.com/user-attachments/assets/b084ce4f-9ec8-4096-af02-b997d712edbb" /> <img width="472" height="962" alt="image" src="https://github.com/user-attachments/assets/2ba5ccfd-71cc-4375-b87b-f74af64cec20" /> After: <img width="1822" height="2632" alt="image" src="https://github.com/user-attachments/assets/fa70bdd2-63c5-4259-aad5-d060f498a494" /> <img width="900" height="1120" alt="image" src="https://github.com/user-attachments/assets/1d267828-c260-4220-9493-a28f012898f9" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Style** * Clearer section separation using visual separators for wrapper creation/edit flows. * Adjusted column selector sizing for improved responsiveness. * Secure fields show a loading state while retrieving secrets. * **Refactor** * Unified form section layout across wrapper management for more consistent, scannable configuration panels. * Simplified data-target and schema areas for easier navigation and editing. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46283?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
fdd6131567
commit
d4274ea27e
4 files changed
+338
-406
No files matched your search
+140
-174
@@ -4,8 +4,6 @@ import { SubmitHandler, useForm, useWatch } from 'react-hook-form'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
@@ -18,18 +16,15 @@ import {
|
||||
SheetTitle,
|
||||
} from 'ui'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import {
|
||||
PageSection,
|
||||
PageSectionContent,
|
||||
PageSectionDescription,
|
||||
PageSectionMeta,
|
||||
PageSectionSummary,
|
||||
PageSectionTitle,
|
||||
} from 'ui-patterns/PageSection'
|
||||
import * as z from 'zod'
|
||||
|
||||
import { CreateWrapperSheetProps } from './CreateWrapperSheet'
|
||||
import InputField from './InputField'
|
||||
import {
|
||||
FormSection,
|
||||
FormSectionContent,
|
||||
FormSectionLabel,
|
||||
} from '@/components/ui/Forms/FormSection'
|
||||
import { useSchemaCreateMutation } from '@/data/database/schema-create-mutation'
|
||||
import { useSchemasQuery } from '@/data/database/schemas-query'
|
||||
import { useFDWCreateMutation } from '@/data/fdw/fdw-create-mutation'
|
||||
@@ -258,173 +253,144 @@ export const CreateIcebergWrapperSheet = ({
|
||||
<SheetTitle>Create a {wrapperMeta.label} wrapper</SheetTitle>
|
||||
</SheetHeader>
|
||||
<SheetSection className="grow overflow-y-auto">
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Wrapper Configuration</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="wrapper_name"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="horizontal"
|
||||
label="Wrapper Name"
|
||||
description={
|
||||
wrapperName.length > 0 ? (
|
||||
<>
|
||||
Your wrapper's server name will be{' '}
|
||||
<code className="text-code-inline">{wrapperName}_server</code>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Data target</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="target"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout layout="vertical">
|
||||
<div>
|
||||
<RadioGroupStacked value={field.value} onValueChange={field.onChange}>
|
||||
<RadioGroupStackedItem
|
||||
key="S3Tables"
|
||||
value="S3Tables"
|
||||
label="AWS S3 Tables"
|
||||
showIndicator={false}
|
||||
>
|
||||
<div className="flex gap-x-5">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-foreground-light text-left">
|
||||
AWS S3 storage that's optimized for analytics workloads.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroupStackedItem>
|
||||
<RadioGroupStackedItem
|
||||
key="R2Catalog"
|
||||
value="R2Catalog"
|
||||
label="Cloudflare R2 Catalog"
|
||||
showIndicator={false}
|
||||
>
|
||||
<div className="flex gap-x-5">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-foreground-light text-left">
|
||||
Managed Apache Iceberg built directly into your R2 bucket.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroupStackedItem>
|
||||
<RadioGroupStackedItem
|
||||
key="IcebergRestCatalog"
|
||||
value="IcebergRestCatalog"
|
||||
label="Iceberg REST Catalog"
|
||||
showIndicator={false}
|
||||
>
|
||||
<div className="flex gap-x-5">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-foreground-light text-left">
|
||||
Can be used with any S3-compatible storage.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroupStackedItem>
|
||||
</RadioGroupStacked>
|
||||
</div>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>{wrapperMeta.label} Configuration</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
<Card>
|
||||
{targetOptions.map((option) =>
|
||||
option.hidden ? (
|
||||
<input
|
||||
key={`${option.name}-${option.required}-${option.hidden}`}
|
||||
type="hidden"
|
||||
// @ts-expect-error Can't reconcile with form schema
|
||||
{...form.register(option.name)}
|
||||
/>
|
||||
) : (
|
||||
<CardContent key={`${option.name}-${option.required}-${option.hidden}`}>
|
||||
<InputField control={form.control} option={option} />
|
||||
</CardContent>
|
||||
)
|
||||
<FormSection header={<FormSectionLabel>Wrapper Configuration</FormSectionLabel>}>
|
||||
<FormSectionContent className="flex flex-col space-y-2" loading={false}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="wrapper_name"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="horizontal"
|
||||
label="Wrapper Name"
|
||||
description={
|
||||
wrapperName.length > 0 ? (
|
||||
<>
|
||||
Your wrapper's server name will be{' '}
|
||||
<code className="text-code-inline">{wrapperName}_server</code>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
</Card>
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Foreign Schema</PageSectionTitle>
|
||||
<PageSectionDescription>
|
||||
/>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
<FormSection header={<FormSectionLabel>Data target</FormSectionLabel>}>
|
||||
<FormSectionContent className="flex flex-col space-y-2" loading={false}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="target"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout layout="vertical">
|
||||
<div>
|
||||
<RadioGroupStacked value={field.value} onValueChange={field.onChange}>
|
||||
<RadioGroupStackedItem
|
||||
key="S3Tables"
|
||||
value="S3Tables"
|
||||
label="AWS S3 Tables"
|
||||
showIndicator={false}
|
||||
>
|
||||
<div className="flex gap-x-5">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-foreground-light text-left">
|
||||
AWS S3 storage that's optimized for analytics workloads.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroupStackedItem>
|
||||
<RadioGroupStackedItem
|
||||
key="R2Catalog"
|
||||
value="R2Catalog"
|
||||
label="Cloudflare R2 Catalog"
|
||||
showIndicator={false}
|
||||
>
|
||||
<div className="flex gap-x-5">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-foreground-light text-left">
|
||||
Managed Apache Iceberg built directly into your R2 bucket.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroupStackedItem>
|
||||
<RadioGroupStackedItem
|
||||
key="IcebergRestCatalog"
|
||||
value="IcebergRestCatalog"
|
||||
label="Iceberg REST Catalog"
|
||||
showIndicator={false}
|
||||
>
|
||||
<div className="flex gap-x-5">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-foreground-light text-left">
|
||||
Can be used with any S3-compatible storage.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</RadioGroupStackedItem>
|
||||
</RadioGroupStacked>
|
||||
</div>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
|
||||
<FormSection
|
||||
header={<FormSectionLabel>{wrapperMeta.label} Configuration</FormSectionLabel>}
|
||||
>
|
||||
<FormSectionContent className="flex flex-col space-y-2" loading={false}>
|
||||
{targetOptions.map((option) =>
|
||||
option.hidden ? (
|
||||
<input
|
||||
key={`${option.name}-${option.required}-${option.hidden}`}
|
||||
type="hidden"
|
||||
// @ts-expect-error Can't reconcile with form schema
|
||||
{...form.register(option.name)}
|
||||
/>
|
||||
) : (
|
||||
<InputField
|
||||
key={`${option.name}-${option.required}-${option.hidden}`}
|
||||
control={form.control}
|
||||
option={option}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
<FormSection
|
||||
header={
|
||||
<FormSectionLabel>
|
||||
<p>Foreign Schema</p>
|
||||
<p className="text-foreground-light mt-2 w-[90%]">
|
||||
You can query your data from the foreign tables in the specified schema after
|
||||
the wrapper is created.
|
||||
</PageSectionDescription>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
<Card>
|
||||
<CardContent>
|
||||
{wrapperMeta.sourceSchemaOption && (
|
||||
<InputField
|
||||
control={form.control}
|
||||
option={wrapperMeta.sourceSchemaOption}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<InputField
|
||||
control={form.control}
|
||||
option={{
|
||||
name: 'target_schema',
|
||||
label: 'Specify a new schema to create all wrapper tables in',
|
||||
description:
|
||||
'A new schema will be created. For security purposes, the wrapper tables from the foreign schema cannot be created within an existing schema.',
|
||||
required: true,
|
||||
encrypted: false,
|
||||
secureEntry: false,
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
</p>
|
||||
</FormSectionLabel>
|
||||
}
|
||||
>
|
||||
<FormSectionContent className="flex flex-col space-y-2" loading={false}>
|
||||
{wrapperMeta.sourceSchemaOption && (
|
||||
<InputField control={form.control} option={wrapperMeta.sourceSchemaOption} />
|
||||
)}
|
||||
<InputField
|
||||
control={form.control}
|
||||
option={{
|
||||
name: 'target_schema',
|
||||
label: 'Specify a new schema to create all wrapper tables in',
|
||||
description:
|
||||
'A new schema will be created. For security purposes, the wrapper tables from the foreign schema cannot be created within an existing schema.',
|
||||
required: true,
|
||||
encrypted: false,
|
||||
secureEntry: false,
|
||||
}}
|
||||
/>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
</SheetSection>
|
||||
|
||||
<SheetFooter>
|
||||
|
||||
@@ -6,35 +6,30 @@ import { SubmitHandler, useFieldArray, useForm, useWatch } from 'react-hook-form
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
Input,
|
||||
RadioGroupStacked,
|
||||
RadioGroupStackedItem,
|
||||
Separator,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetSection,
|
||||
SheetTitle,
|
||||
WarningIcon,
|
||||
} from 'ui'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import {
|
||||
PageSection,
|
||||
PageSectionContent,
|
||||
PageSectionDescription,
|
||||
PageSectionMeta,
|
||||
PageSectionSummary,
|
||||
PageSectionTitle,
|
||||
} from 'ui-patterns/PageSection'
|
||||
import * as z from 'zod'
|
||||
|
||||
import InputField from './InputField'
|
||||
import { WrapperMeta } from './Wrappers.types'
|
||||
import { FormattedWrapperTable, getWrapperCreationFormSchema, NewTable } from './Wrappers.utils'
|
||||
import WrapperTableEditor from './WrapperTableEditor'
|
||||
import {
|
||||
FormSection,
|
||||
FormSectionContent,
|
||||
FormSectionLabel,
|
||||
} from '@/components/ui/Forms/FormSection'
|
||||
import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
|
||||
import { useSchemaCreateMutation } from '@/data/database/schema-create-mutation'
|
||||
import { invalidateSchemasQuery, useSchemasQuery } from '@/data/database/schemas-query'
|
||||
@@ -221,70 +216,51 @@ export const CreateWrapperSheet = ({
|
||||
<SheetHeader>
|
||||
<SheetTitle>Create a {wrapperMeta.label} wrapper</SheetTitle>
|
||||
</SheetHeader>
|
||||
<SheetSection className="grow overflow-y-auto">
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Wrapper Configuration</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<FormField
|
||||
control={form.control}
|
||||
<div className="flex-grow overflow-y-auto">
|
||||
<FormSection header={<FormSectionLabel>Wrapper Configuration</FormSectionLabel>}>
|
||||
<FormSectionContent className="flex flex-col space-y-2" loading={false}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="wrapper_name"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="vertical"
|
||||
label="Wrapper Name"
|
||||
name="wrapper_name"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="vertical"
|
||||
label="Wrapper Name"
|
||||
name="wrapper_name"
|
||||
description={
|
||||
wrapper_name.length > 0 ? (
|
||||
<>
|
||||
Your wrapper's server name will be{' '}
|
||||
<code className="text-code-inline">{wrapper_name}_server</code>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<Input id="wrapper_name" {...field} />
|
||||
</FormControl>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>{wrapperMeta.label} Configuration</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
<Card>
|
||||
{wrapperMeta.server.options
|
||||
.filter((option) => !option.hidden)
|
||||
.map((option) => (
|
||||
<CardContent key={option.name}>
|
||||
<InputField option={option} control={form.control} />
|
||||
</CardContent>
|
||||
))}
|
||||
</Card>
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Data target</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
description={
|
||||
wrapper_name.length > 0 ? (
|
||||
<>
|
||||
Your wrapper's server name will be{' '}
|
||||
<code className="text-code-inline">{wrapper_name}_server</code>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<Input id="wrapper_name" {...field} />
|
||||
</FormControl>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
<Separator />
|
||||
<FormSection
|
||||
header={<FormSectionLabel>{wrapperMeta.label} Configuration</FormSectionLabel>}
|
||||
>
|
||||
<FormSectionContent className="flex flex-col space-y-2" loading={false}>
|
||||
{wrapperMeta.server.options
|
||||
.filter((option) => !option.hidden)
|
||||
.map((option) => (
|
||||
<InputField option={option} control={form.control} key={option.name} />
|
||||
))}
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
<Separator />
|
||||
<FormSection header={<FormSectionLabel>Data target</FormSectionLabel>}>
|
||||
<FormSectionContent className="flex flex-col space-y-2" loading={false}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mode"
|
||||
@@ -360,84 +336,90 @@ export const CreateWrapperSheet = ({
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
<Separator />
|
||||
{mode === 'tables' && (
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Foreign Tables</PageSectionTitle>
|
||||
<PageSectionDescription>
|
||||
<FormSection
|
||||
header={
|
||||
<FormSectionLabel>
|
||||
<p>Foreign Tables</p>
|
||||
<p className="text-foreground-light mt-2 w-[90%]">
|
||||
You can query your data from these foreign tables after the wrapper is
|
||||
created
|
||||
</PageSectionDescription>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent className="flex flex-col space-y-2">
|
||||
{tablesField.map((t, tableIndex) => {
|
||||
// FIXME: make inference work
|
||||
const table = t as unknown as FormattedWrapperTable
|
||||
return (
|
||||
<div
|
||||
key={t.id}
|
||||
className="flex items-center justify-between px-4 py-2 border rounded-md border-control"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm">
|
||||
{table.schema_name}.{table.table_name}
|
||||
</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Columns:{' '}
|
||||
{(table.columns ?? []).map((column: any) => column.name).join(', ')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
type="default"
|
||||
className="px-1"
|
||||
icon={<Edit />}
|
||||
onClick={() => {
|
||||
setSelectedTableToEdit(table)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="default"
|
||||
className="px-1"
|
||||
icon={<Trash />}
|
||||
onClick={() => {
|
||||
removeTable(tableIndex)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="default" onClick={() => setSelectedTableToEdit(NewTable)}>
|
||||
Add foreign table
|
||||
</Button>
|
||||
</div>
|
||||
{tablesField.length === 0 && errors.tables && (
|
||||
<p className="text-sm text-right text-red-900">
|
||||
{errors.tables.message?.toString()}
|
||||
</p>
|
||||
)}
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
</FormSectionLabel>
|
||||
}
|
||||
>
|
||||
<FormSectionContent className="flex flex-col space-y-2" loading={false}>
|
||||
<div className="flex flex-col space-y-2">
|
||||
{tablesField.map((t, tableIndex) => {
|
||||
// FIXME: make inference work
|
||||
const table = t as unknown as FormattedWrapperTable
|
||||
return (
|
||||
<div
|
||||
key={t.id}
|
||||
className="flex items-center justify-between px-4 py-2 border rounded-md border-control"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm">
|
||||
{table.schema_name}.{table.table_name}
|
||||
</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Columns:{' '}
|
||||
{(table.columns ?? []).map((column: any) => column.name).join(', ')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
type="default"
|
||||
className="px-1"
|
||||
icon={<Edit />}
|
||||
onClick={() => {
|
||||
setSelectedTableToEdit(table)
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="default"
|
||||
className="px-1"
|
||||
icon={<Trash />}
|
||||
onClick={() => {
|
||||
removeTable(tableIndex)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="default" onClick={() => setSelectedTableToEdit(NewTable)}>
|
||||
Add foreign table
|
||||
</Button>
|
||||
</div>
|
||||
{tablesField.length === 0 && errors.tables && (
|
||||
<p className="text-sm text-right text-red-900">
|
||||
{errors.tables.message?.toString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
)}
|
||||
<Separator />
|
||||
{mode === 'schema' && (
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Foreign Schema</PageSectionTitle>
|
||||
<PageSectionDescription>
|
||||
<FormSection
|
||||
header={
|
||||
<FormSectionLabel>
|
||||
<p>Foreign Schema</p>
|
||||
<p className="text-foreground-light mt-2 w-[90%]">
|
||||
You can query your data from the foreign tables in the specified schema
|
||||
after the wrapper is created.
|
||||
</PageSectionDescription>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
</p>
|
||||
</FormSectionLabel>
|
||||
}
|
||||
>
|
||||
<FormSectionContent className="flex flex-col space-y-2" loading={false}>
|
||||
{wrapperMeta.sourceSchemaOption &&
|
||||
!wrapperMeta.sourceSchemaOption?.readOnly && (
|
||||
// Hide the field if the source schema is read-only
|
||||
@@ -462,10 +444,10 @@ export const CreateWrapperSheet = ({
|
||||
control={form.control}
|
||||
/>
|
||||
</div>
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
)}
|
||||
</SheetSection>
|
||||
</div>
|
||||
<SheetFooter>
|
||||
<Button
|
||||
size="tiny"
|
||||
|
||||
@@ -7,27 +7,17 @@ import { SubmitHandler, useFieldArray, useForm, useWatch } from 'react-hook-form
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
Input,
|
||||
Separator,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetSection,
|
||||
SheetTitle,
|
||||
} from 'ui'
|
||||
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import {
|
||||
PageSection,
|
||||
PageSectionContent,
|
||||
PageSectionDescription,
|
||||
PageSectionMeta,
|
||||
PageSectionSummary,
|
||||
PageSectionTitle,
|
||||
} from 'ui-patterns/PageSection'
|
||||
import * as z from 'zod'
|
||||
|
||||
import InputField from './InputField'
|
||||
@@ -41,6 +31,11 @@ import {
|
||||
} from './Wrappers.utils'
|
||||
import WrapperTableEditor from './WrapperTableEditor'
|
||||
import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
|
||||
import {
|
||||
FormSection,
|
||||
FormSectionContent,
|
||||
FormSectionLabel,
|
||||
} from '@/components/ui/Forms/FormSection'
|
||||
import { invalidateSchemasQuery } from '@/data/database/schemas-query'
|
||||
import { useFDWUpdateMutation } from '@/data/fdw/fdw-update-mutation'
|
||||
import { FDW } from '@/data/fdw/fdws-query'
|
||||
@@ -205,79 +200,68 @@ export const EditWrapperSheet = ({
|
||||
Edit {wrapperMeta.label} wrapper: {wrapper.name}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<SheetSection className="grow overflow-y-auto">
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Wrapper Configuration</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<FormField
|
||||
<div className="grow overflow-y-auto">
|
||||
<FormSection header={<FormSectionLabel>Wrapper Configuration</FormSectionLabel>}>
|
||||
<FormSectionContent className="flex flex-col space-y-2" loading={false}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="wrapper_name"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="vertical"
|
||||
label="Wrapper Name"
|
||||
description={
|
||||
wrapper_name !== initialValues.wrapper_name ? (
|
||||
<>
|
||||
Your wrapper's server name will be updated to{' '}
|
||||
<code className="text-code-inline">{wrapper_name}_server</code>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Your wrapper's server name is{' '}
|
||||
<code className="text-code-inline">{wrapper_name}_server</code>
|
||||
</>
|
||||
)
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
<Separator />
|
||||
|
||||
<FormSection
|
||||
header={<FormSectionLabel>{wrapperMeta.label} Configuration</FormSectionLabel>}
|
||||
>
|
||||
<FormSectionContent className="flex flex-col space-y-2" loading={false}>
|
||||
{wrapperMeta.server.options
|
||||
.filter((option) => !option.hidden)
|
||||
.map((option) => (
|
||||
<InputField
|
||||
key={option.name}
|
||||
option={option}
|
||||
control={form.control}
|
||||
name="wrapper_name"
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
layout="vertical"
|
||||
label="Wrapper Name"
|
||||
description={
|
||||
wrapper_name !== initialValues.wrapper_name ? (
|
||||
<>
|
||||
Your wrapper's server name will be updated to{' '}
|
||||
<code className="text-code-inline">{wrapper_name}_server</code>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Your wrapper's server name is{' '}
|
||||
<code className="text-code-inline">{wrapper_name}_server</code>
|
||||
</>
|
||||
)
|
||||
}
|
||||
>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
loading={option.secureEntry ? isLoadingSecrets : undefined}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>{wrapperMeta.label} Configuration</PageSectionTitle>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent>
|
||||
<Card>
|
||||
{wrapperMeta.server.options
|
||||
.filter((option) => !option.hidden)
|
||||
.map((option) => (
|
||||
<CardContent key={option.name}>
|
||||
<InputField
|
||||
option={option}
|
||||
control={form.control}
|
||||
loading={option.secureEntry ? isLoadingSecrets : undefined}
|
||||
/>
|
||||
</CardContent>
|
||||
))}
|
||||
</Card>
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
<PageSection>
|
||||
<PageSectionMeta>
|
||||
<PageSectionSummary>
|
||||
<PageSectionTitle>Foreign Tables</PageSectionTitle>
|
||||
<PageSectionDescription>
|
||||
))}
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
<Separator />
|
||||
<FormSection
|
||||
header={
|
||||
<FormSectionLabel>
|
||||
<p>Foreign Tables</p>
|
||||
<p className="text-foreground-light mt-2 w-[90%]">
|
||||
You can query your data from these foreign tables after the wrapper is created
|
||||
</PageSectionDescription>
|
||||
</PageSectionSummary>
|
||||
</PageSectionMeta>
|
||||
<PageSectionContent className="flex flex-col space-y-2">
|
||||
</p>
|
||||
</FormSectionLabel>
|
||||
}
|
||||
>
|
||||
<FormSectionContent className="flex flex-col space-y-2" loading={false}>
|
||||
{tablesField.map((t, tableIndex) => {
|
||||
// FIXME: make inference work
|
||||
const table = t as unknown as FormattedWrapperTable
|
||||
@@ -327,9 +311,9 @@ export const EditWrapperSheet = ({
|
||||
{errors.tables.message?.toString()}
|
||||
</p>
|
||||
)}
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
</SheetSection>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
</div>
|
||||
<SheetFooter>
|
||||
<Button
|
||||
size="tiny"
|
||||
|
||||
@@ -443,7 +443,7 @@ const TableForm = ({
|
||||
badgeLimit="wrap"
|
||||
showIcon={false}
|
||||
deletableBadge
|
||||
className="w-full min-w-lg!"
|
||||
className="w-full"
|
||||
/>
|
||||
<MultiSelectorContent>
|
||||
<MultiSelectorList>
|
||||
|
||||
Reference in new issue
Block a user