| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364 |
- import { zodResolver } from '@hookform/resolvers/zod'
- import { useParams } from 'common'
- import { Plus, X } from 'lucide-react'
- import { Fragment, useState } from 'react'
- import { SubmitHandler, useFieldArray, useForm } from 'react-hook-form'
- import { toast } from 'sonner'
- import {
- Button,
- DialogSectionSeparator,
- Form,
- FormControl,
- FormField,
- FormInputGroupInput,
- Input,
- InputGroup,
- InputGroupAddon,
- Select,
- SelectContent,
- SelectItem,
- SelectSeparator,
- SelectTrigger,
- SelectValue,
- Sheet,
- SheetContent,
- SheetFooter,
- SheetHeader,
- SheetSection,
- SheetTitle,
- } from 'ui'
- import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
- import { z } from 'zod'
- import { COLUMN_TYPE_FIELDS, COLUMN_TYPES } from './CreateTableSheet.constants'
- import { createFormSchema } from './CreateTableSheet.schema'
- import { useIcebergNamespaceCreateMutation } from '@/data/storage/iceberg-namespace-create-mutation'
- import {
- NamespaceTableFields,
- useIcebergNamespaceTableCreateMutation,
- } from '@/data/storage/iceberg-namespace-table-create-mutation'
- import { useIcebergNamespaceTablesQuery } from '@/data/storage/iceberg-namespace-tables-query'
- import { useIcebergNamespacesQuery } from '@/data/storage/iceberg-namespaces-query'
- const formId = 'create-namespace-table'
- const NEW_NAMESPACE_MARKER = 'new-namespace'
- interface CreateTableSheetProps {
- open: boolean
- onOpenChange: (value: boolean) => void
- }
- export const CreateTableSheet = ({ open, onOpenChange }: CreateTableSheetProps) => {
- const { ref: projectRef, bucketId } = useParams()
- const [isCreating, setIsCreating] = useState(false)
- const FormSchema = createFormSchema()
- const defaultValues = {
- namespace: '',
- newNamespace: undefined,
- name: '',
- columns: [{ name: '', type: 'string' as any }],
- }
- const form = useForm<z.infer<typeof FormSchema>>({
- resolver: zodResolver(FormSchema as any),
- defaultValues,
- mode: 'onChange',
- })
- const { namespace } = form.watch()
- const {
- fields: columns,
- append: appendColumn,
- remove: removeColumn,
- } = useFieldArray({ control: form.control, name: 'columns' })
- const { data: namespaces = [] } = useIcebergNamespacesQuery({ projectRef, warehouse: bucketId })
- const { data: tables = [] } = useIcebergNamespaceTablesQuery(
- {
- projectRef,
- warehouse: bucketId,
- namespace,
- },
- { enabled: namespace !== NEW_NAMESPACE_MARKER }
- )
- const { mutateAsync: createNamespace } = useIcebergNamespaceCreateMutation()
- const { mutateAsync: createTable } = useIcebergNamespaceTableCreateMutation()
- const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (values) => {
- if (!bucketId) return console.error('Bucket ID is missing')
- if (namespaces.includes(values.newNamespace ?? '')) {
- return form.setError('newNamespace', { message: 'Namespace name already exists' })
- }
- if (tables.includes(values.name ?? '')) {
- return form.setError('name', { message: 'Table name already exists' })
- }
- const isCreatingNewNamespace =
- values.namespace === NEW_NAMESPACE_MARKER && !!values.newNamespace
- try {
- setIsCreating(true)
- if (isCreatingNewNamespace) {
- await createNamespace({
- projectRef,
- warehouse: bucketId,
- namespace: values.newNamespace as string,
- })
- }
- const fields = values.columns.map((column, idx) => {
- return {
- id: idx + 1,
- name: column.name,
- type:
- column.type === 'decimal'
- ? `decimal(${column.precision}, ${column.scale})`
- : column.type === 'fixed'
- ? `fixed[${column.length}]`
- : column.type,
- required: false,
- }
- }) as NamespaceTableFields
- await createTable({
- projectRef,
- warehouse: bucketId,
- namespace: isCreatingNewNamespace ? (values.newNamespace as string) : values.namespace,
- name: values.name,
- fields,
- })
- toast.success(`Successfully created table in ${values.newNamespace ?? values.namespace}!`)
- onOpenChange(false)
- form.reset(defaultValues)
- } catch (error) {
- } finally {
- setIsCreating(false)
- }
- }
- return (
- <Sheet open={open} onOpenChange={onOpenChange}>
- <Form {...form}>
- <form id={formId} className="flex flex-col gap-4" onSubmit={form.handleSubmit(onSubmit)}>
- <SheetContent aria-describedby={undefined} className="flex flex-col gap-0">
- <SheetHeader className="shrink-0 flex items-center gap-4">
- <SheetTitle>Create a new table</SheetTitle>
- </SheetHeader>
- <SheetSection className="overflow-auto grow p-0">
- <div className="flex flex-col gap-y-4 py-4 px-5">
- <FormField
- name="namespace"
- control={form.control}
- render={({ field }) => (
- <FormItemLayout
- name="namespace"
- label="Select a namespace to create your table in"
- >
- <FormControl>
- <Select
- value={field.value}
- onValueChange={(value) => {
- field.onChange(value)
- form.resetField('newNamespace')
- }}
- >
- <SelectTrigger>
- <SelectValue placeholder="Select a namespace" />
- </SelectTrigger>
- <SelectContent>
- {namespaces.map((x) => (
- <SelectItem key={x} value={x}>
- {x}
- </SelectItem>
- ))}
- {namespaces.length > 0 && <SelectSeparator />}
- <SelectItem value={NEW_NAMESPACE_MARKER}>
- <div className="flex items-center gap-x-2">
- <Plus size={14} />
- <p>Create a new namespace</p>
- </div>
- </SelectItem>
- </SelectContent>
- </Select>
- </FormControl>
- </FormItemLayout>
- )}
- />
- {namespace === NEW_NAMESPACE_MARKER && (
- <FormField
- name="newNamespace"
- control={form.control}
- render={({ field }) => (
- <FormItemLayout name="newNamespace" label="Name of new namespace">
- <FormControl>
- <Input {...field} placeholder="Provide a name for your new namespace" />
- </FormControl>
- </FormItemLayout>
- )}
- />
- )}
- </div>
- <DialogSectionSeparator />
- {!!namespace && (
- <div className="px-5 py-4 flex flex-col gap-y-4">
- <FormField
- name="name"
- control={form.control}
- render={({ field }) => (
- <FormItemLayout name="name" label="Name of table">
- <FormControl>
- <Input {...field} placeholder="Provide a name for your new table" />
- </FormControl>
- </FormItemLayout>
- )}
- />
- <div className="flex flex-col gap-y-2">
- <div className="flex items-center justify-between">
- <p className="text-sm">Columns</p>
- <Button
- type="default"
- icon={<Plus />}
- onClick={() => appendColumn({ name: '', type: 'string' })}
- >
- Add column
- </Button>
- </div>
- {columns.length === 0 ? (
- <div className="flex items-center justify-center rounded-sm border border-strong border-dashed py-4 text-foreground-lighter text-sm">
- Add a column to your table
- </div>
- ) : (
- <>
- <div className="grid grid-cols-[1fr_1fr_32px]">
- <p className="text-xs text-foreground-lighter">Name</p>
- <p className="text-xs text-foreground-lighter">Type</p>
- </div>
- {columns.map((_, idx) => {
- const columnType = form.watch(`columns.${idx}.type`)
- const additionalFields =
- COLUMN_TYPE_FIELDS[columnType as keyof typeof COLUMN_TYPE_FIELDS] ?? []
- return (
- <Fragment key={`column-${idx}`}>
- <div className="grid grid-cols-[1fr_1fr_32px] gap-x-1">
- <FormField
- control={form.control}
- name={`columns.${idx}.name`}
- render={({ field }) => (
- <FormItemLayout>
- <FormControl>
- <Input
- {...field}
- placeholder="Provide a column name"
- disabled={isCreating}
- className="h-auto"
- />
- </FormControl>
- </FormItemLayout>
- )}
- />
- <FormField
- control={form.control}
- name={`columns.${idx}.type`}
- render={({ field }) => (
- <FormControl>
- <Select value={field.value} onValueChange={field.onChange}>
- <SelectTrigger className="h-auto">
- <SelectValue placeholder="Select a type" />
- </SelectTrigger>
- <SelectContent>
- {COLUMN_TYPES.map((x) => (
- <SelectItem key={x} value={x}>
- {x}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </FormControl>
- )}
- />
- <div className="flex items-center justify-center">
- <Button
- type="text"
- size="tiny"
- icon={<X strokeWidth={1.5} size={14} />}
- className="w-6 h-6"
- onClick={() => removeColumn(idx)}
- />
- </div>
- {additionalFields.length > 0 && (
- <div className="col-span-full flex items-center mt-2">
- <div className="flex items-center justify-end gap-1 w-[85%] ">
- {additionalFields.map((x) => (
- <FormField
- control={form.control}
- key={`columns.${idx}.${x.name}`}
- name={`columns.${idx}.${x.name}` as any}
- render={({ field }) => (
- <FormItemLayout>
- <FormControl>
- <InputGroup>
- <InputGroupAddon align="inline-start">
- {x.name}
- </InputGroupAddon>
- <FormInputGroupInput
- {...field}
- type={x.type === 'number' ? 'number' : 'text'}
- disabled={isCreating}
- className="h-[34px] rounded-l-none"
- onChange={(event) =>
- field.onChange(
- isNaN(event.target.valueAsNumber)
- ? null
- : event.target.valueAsNumber
- )
- }
- />
- </InputGroup>
- </FormControl>
- </FormItemLayout>
- )}
- />
- ))}
- </div>
- <div className="w-4 h-[1.6rem] border-r border-b rounded-br mr-3 border-control -translate-y-3" />
- </div>
- )}
- </div>
- </Fragment>
- )
- })}
- </>
- )}
- </div>
- </div>
- )}
- </SheetSection>
- <SheetFooter>
- <Button
- disabled={isCreating}
- type="default"
- onClick={() => {
- onOpenChange(false)
- form.reset(defaultValues)
- }}
- >
- Cancel
- </Button>
- <Button form={formId} htmlType="submit" loading={isCreating}>
- Create table
- </Button>
- </SheetFooter>
- </SheetContent>
- </form>
- </Form>
- </Sheet>
- )
- }
|