| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506 |
- // @ts-nocheck
- import { zodResolver } from '@hookform/resolvers/zod'
- import { useQueryClient } from '@tanstack/react-query'
- import { Edit, Trash } from 'lucide-react'
- import { useEffect, useState } from 'react'
- import { SubmitHandler, useFieldArray, useForm, useWatch } from 'react-hook-form'
- import { toast } from 'sonner'
- import {
- Button,
- Card,
- CardContent,
- Form,
- FormControl,
- FormField,
- Input,
- RadioGroupStacked,
- RadioGroupStackedItem,
- 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 { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query'
- import { useSchemaCreateMutation } from '@/data/database/schema-create-mutation'
- import { invalidateSchemasQuery, useSchemasQuery } from '@/data/database/schemas-query'
- import { useFDWCreateMutation } from '@/data/fdw/fdw-create-mutation'
- import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
- import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
- import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
- const FORM_ID = 'create-wrapper-form'
- export interface CreateWrapperSheetProps {
- wrapperMeta: WrapperMeta
- onDirty: (isDirty: boolean) => void
- onClose: () => void
- onCloseWithConfirmation: () => void
- }
- export const CreateWrapperSheet = ({
- wrapperMeta,
- onDirty,
- onClose,
- onCloseWithConfirmation,
- }: CreateWrapperSheetProps) => {
- const queryClient = useQueryClient()
- const { data: project } = useSelectedProjectQuery()
- const { data: org } = useSelectedOrganizationQuery()
- const { mutate: sendEvent } = useSendEventMutation()
- const [selectedTableToEdit, setSelectedTableToEdit] = useState<
- FormattedWrapperTable | undefined
- >()
- const { data: extensions } = useDatabaseExtensionsQuery({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- })
- const wrappersExtension = extensions?.find((ext) => ext.name === 'wrappers')
- // The import foreign schema requires a minimum extension version of 0.5.0
- const hasRequiredVersionForeignSchema = wrappersExtension?.installed_version
- ? wrappersExtension?.installed_version >= '0.5.0'
- : false
- const { data: schemas } = useSchemasQuery({
- projectRef: project?.ref!,
- connectionString: project?.connectionString,
- })
- const initialValues = {
- wrapper_name: '',
- server_name: '',
- mode: wrapperMeta.tables.length > 0 ? 'tables' : 'schema',
- source_schema: wrapperMeta.sourceSchemaOption?.defaultValue ?? '',
- target_schema: '',
- ...Object.fromEntries(
- wrapperMeta.server.options.map((option) => [option.name, option.defaultValue ?? ''])
- ),
- tables: [] as Array<FormattedWrapperTable>,
- }
- const formSchema = getWrapperCreationFormSchema(wrapperMeta)
- type FormSchema = z.infer<typeof formSchema>
- const form = useForm<FormSchema>({
- defaultValues: initialValues,
- resolver: zodResolver(formSchema as any),
- })
- const { getValues, setError } = form
- const { errors, isDirty, isSubmitting } = form.formState
- useEffect(() => {
- onDirty(isDirty)
- }, [onDirty, isDirty])
- const {
- fields: tablesField,
- append: appendTable,
- remove: removeTable,
- insert: insertTable,
- } = useFieldArray({
- control: form.control,
- name: 'tables',
- })
- const { mutateAsync: createSchema, isPending: isCreatingSchema } = useSchemaCreateMutation()
- const onUpdateTable = (values: FormattedWrapperTable) => {
- if (values.index !== undefined) {
- removeTable(values.index)
- insertTable(values.index, values)
- } else {
- appendTable(values)
- }
- setSelectedTableToEdit(undefined)
- }
- const { mutateAsync: createFDW, isPending: isCreatingWrapper } = useFDWCreateMutation({
- onSuccess: () => {
- toast.success(`Successfully created ${wrapperMeta?.label} foreign data wrapper`)
- const { tables } = getValues()
- const hasNewSchema = (tables as Record<string, any>[]).some((table) => table.is_new_schema)
- if (hasNewSchema) invalidateSchemasQuery(queryClient, project?.ref)
- onClose()
- form.reset()
- },
- })
- const onSubmit: SubmitHandler<FormSchema> = async (values) => {
- const { mode, tables = [], ...wrapperValues } = values
- if (mode === 'tables' && tables.length === 0) {
- setError('tables', {
- type: 'validate',
- message: 'Please provide at least one table.',
- })
- return
- }
- if (mode === 'schema') {
- const foundSchema = schemas?.find((s) => s.name === wrapperValues.target_schema)
- if (foundSchema) {
- setError('target_schema', {
- type: 'validate',
- message: 'This schema already exists. Please specify a unique schema name.',
- })
- return
- }
- }
- try {
- if (mode === 'schema') {
- await createSchema({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- name: wrapperValues.target_schema,
- })
- }
- await createFDW({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- wrapperMeta,
- formState: {
- ...wrapperValues,
- server_name: `${wrapperValues.wrapper_name}_server`,
- briven_target_schema: mode === 'schema' ? wrapperValues.target_schema : undefined,
- },
- mode: mode === 'schema' ? (wrapperMeta.sourceSchemaOption ? 'schema' : 'skip') : 'tables',
- tables,
- sourceSchema: wrapperValues.source_schema,
- targetSchema: wrapperValues.target_schema,
- })
- sendEvent({
- action: 'foreign_data_wrapper_created',
- properties: {
- wrapperType: wrapperMeta.label,
- },
- groups: {
- project: project?.ref ?? 'Unknown',
- organization: org?.slug ?? 'Unknown',
- },
- })
- } catch (error) {
- console.error(error)
- // The error will be handled by the mutation onError callback (toast.error)
- }
- }
- const isLoading = isCreatingWrapper || isCreatingSchema
- const wrapper_name = useWatch({ name: 'wrapper_name', control: form.control })
- const mode = useWatch({ name: 'mode', control: form.control })
- return (
- <>
- <div className="h-full" tabIndex={-1}>
- <Form {...form}>
- <form
- id={FORM_ID}
- onSubmit={form.handleSubmit(onSubmit)}
- className="flex flex-col h-full"
- >
- <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}
- 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>
- <FormField
- control={form.control}
- name="mode"
- render={({ field }) => (
- <FormItemLayout layout="vertical">
- <FormControl>
- <RadioGroupStacked
- value={field.value as string}
- onValueChange={field.onChange}
- >
- <RadioGroupStackedItem
- key="tables"
- value="tables"
- disabled={wrapperMeta.tables.length === 0}
- label="Tables"
- showIndicator={false}
- >
- <div className="flex gap-x-5">
- <div className="flex flex-col">
- <p className="text-foreground-light text-left">
- Create foreign tables to query data from {wrapperMeta.label}.
- </p>
- </div>
- </div>
- {wrapperMeta.tables.length === 0 ? (
- <div className="w-full flex gap-x-2 py-2 items-center">
- <WarningIcon />
- <span className="text-xs">
- This wrapper doesn't support using foreign tables.
- </span>
- </div>
- ) : null}
- </RadioGroupStackedItem>
- <RadioGroupStackedItem
- key="schema"
- value="schema"
- disabled={
- !wrapperMeta.canTargetSchema || !hasRequiredVersionForeignSchema
- }
- label="Schema"
- showIndicator={false}
- >
- <div className="flex gap-x-5">
- <div className="flex flex-col">
- <p className="text-foreground-light text-left">
- Create all foreign tables from {wrapperMeta.label} in a
- specified schema.
- </p>
- </div>
- </div>
- {wrapperMeta.canTargetSchema ? (
- hasRequiredVersionForeignSchema ? null : (
- <div className="w-full flex gap-x-2 py-2 items-center">
- <WarningIcon />
- <span className="text-xs text-left">
- This feature requires the{' '}
- <span className="text-brand">wrappers</span> extension to be
- of minimum version of 0.5.0.
- </span>
- </div>
- )
- ) : (
- <div className="w-full flex gap-x-2 py-2 items-center">
- <WarningIcon />
- <span className="text-xs">
- This wrapper doesn't support using a foreign schema.
- </span>
- </div>
- )}
- </RadioGroupStackedItem>
- </RadioGroupStacked>
- </FormControl>
- </FormItemLayout>
- )}
- />
- </PageSectionContent>
- </PageSection>
- {mode === 'tables' && (
- <PageSection>
- <PageSectionMeta>
- <PageSectionSummary>
- <PageSectionTitle>Foreign Tables</PageSectionTitle>
- <PageSectionDescription>
- 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>
- )}
- {mode === 'schema' && (
- <PageSection>
- <PageSectionMeta>
- <PageSectionSummary>
- <PageSectionTitle>Foreign Schema</PageSectionTitle>
- <PageSectionDescription>
- You can query your data from the foreign tables in the specified schema
- after the wrapper is created.
- </PageSectionDescription>
- </PageSectionSummary>
- </PageSectionMeta>
- <PageSectionContent>
- {wrapperMeta.sourceSchemaOption &&
- !wrapperMeta.sourceSchemaOption?.readOnly && (
- // Hide the field if the source schema is read-only
- <InputField
- key="source_schema"
- option={wrapperMeta.sourceSchemaOption}
- control={form.control}
- />
- )}
- <div className="flex flex-col gap-2">
- <InputField
- key="target_schema"
- 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,
- }}
- control={form.control}
- />
- </div>
- </PageSectionContent>
- </PageSection>
- )}
- </SheetSection>
- <SheetFooter>
- <Button
- size="tiny"
- type="default"
- htmlType="button"
- onClick={onCloseWithConfirmation}
- disabled={isLoading}
- >
- Cancel
- </Button>
- <Button
- size="tiny"
- type="primary"
- form={FORM_ID}
- htmlType="submit"
- disabled={isSubmitting || isLoading}
- loading={isLoading}
- >
- Create wrapper
- </Button>
- </SheetFooter>
- </form>
- </Form>
- </div>
- <WrapperTableEditor
- visible={selectedTableToEdit != null}
- tables={wrapperMeta.tables}
- onCancel={() => {
- setSelectedTableToEdit(undefined)
- }}
- onSave={onUpdateTable}
- initialData={selectedTableToEdit}
- />
- </>
- )
- }
|