import React from 'react'; import {ArrowUturnCcwLeft, Plus, TrashBin} from '@gravity-ui/icons'; import type {SelectOption} from '@gravity-ui/uikit'; import { Button, Checkbox, HelpMark, Icon, Popover, Select, Text, TextInput, } from '@gravity-ui/uikit'; import {Controller, useFieldArray, useFormContext, useWatch} from 'react-hook-form'; import {cn} from '../../../../utils/cn'; import {FormFieldError, FormSection} from '../components/layout'; import i18n from '../i18n'; import type {Column, FormMode, FormValues, OriginalTableInfo, TableType} from '../types'; import { generateColumnId, getAutoincrementDisabledMessage, getNotNullDisabledMessage, isSerialCompatible, } from '../utils'; const b = cn('ydb-table-form-dialog'); interface YdbColumnsSectionProps { mode: FormMode; tableType: TableType; types: string[]; pkTypes: Set; keyNullable: boolean; originalInfo?: OriginalTableInfo; } export function YdbColumnsSection({ mode, tableType, types, pkTypes, keyNullable, originalInfo, }: YdbColumnsSectionProps) { const {control, setValue, formState} = useFormContext(); const {fields, append, remove} = useFieldArray({control, name: 'columns'}); const { fields: deletedFields, append: appendDeleted, remove: removeDeleted, } = useFieldArray({control, name: 'deletedColumns', keyName: 'rhfKey'}); const columnsFieldError = formState.errors.columns as | {message?: string; root?: {message?: string}} | undefined; const columnsError = columnsFieldError?.root?.message ?? columnsFieldError?.message; const originalColumns = React.useMemo( () => originalInfo?.columns ?? [], [originalInfo?.columns], ); const primaryOriginalColumns = React.useMemo( () => originalColumns.filter((column) => column.key), [originalColumns], ); const nonPrimaryOriginalColumns = React.useMemo( () => originalColumns.filter((column) => !column.key), [originalColumns], ); const typeOptions = React.useMemo( () => types.map((type) => ({value: type, content: type})), [types], ); const handleAddColumn = React.useCallback(() => { append({ _id: generateColumnId(), name: '', type: 'Utf8', notNull: false, defaultValue: '', withDefaultValue: false, }); }, [append]); const handleTypeChange = React.useCallback( (index: number, nextType: string | undefined) => { if (!nextType) { return; } if (!pkTypes.has(nextType)) { setValue(`columns.${index}.key`, false, {shouldValidate: true}); } if (!isSerialCompatible(nextType)) { setValue(`columns.${index}.autoincrement`, false, {shouldValidate: true}); } }, [pkTypes, setValue], ); const handleKeyChange = React.useCallback( (index: number, nextKey: boolean) => { if (!keyNullable && nextKey) { setValue(`columns.${index}.notNull`, true, {shouldValidate: true}); } if (!nextKey) { setValue(`columns.${index}.autoincrement`, false, {shouldValidate: true}); } }, [keyNullable, setValue], ); const handleAutoincrementChange = React.useCallback( (index: number, value: boolean) => { if (value) { setValue(`columns.${index}.notNull`, true, {shouldValidate: true}); } }, [setValue], ); const handleDeleteOriginalColumn = React.useCallback( (column: Column) => { appendDeleted(column); }, [appendDeleted], ); const showHeader = fields.length > 0 || primaryOriginalColumns.length > 0 || nonPrimaryOriginalColumns.length > 0; const currentTtlColumn = originalInfo?.ttlColumn; const indexedColumns = React.useMemo(() => { const columns = new Set(); originalInfo?.indexes.forEach((index) => { index.columns.forEach((column) => { columns.add(column); }); }); return columns; }, [originalInfo?.indexes]); const primaryKeyColumnNames = primaryOriginalColumns.map((column) => column.name); const partitionKeyColumnNames = originalInfo?.partitionKey ?? []; const showDefaultColumn = tableType === 'row'; return ( {mode === 'update' ? (
{originalInfo?.type === 'column' ? ( ) : null}
) : null}
{showHeader ? (
{i18n('column_name')}
{i18n('column_type')}
{mode === 'create' ? (
{i18n('column_primary-key')} {i18n('tooltip_primary-key')}
) : null}
{i18n('column_not-null')}
{showDefaultColumn ? (
{i18n('column_default')}
) : null}

) : null} {mode === 'update' && primaryOriginalColumns.length > 0 ? ( {primaryOriginalColumns.map((column) => ( ))}
) : null} {mode === 'update' && nonPrimaryOriginalColumns.length > 0 ? ( {nonPrimaryOriginalColumns.map((column) => { const deletedIndex = deletedFields.findIndex( (deleted) => deleted.name === column.name, ); const isDeleting = deletedIndex >= 0; return ( handleDeleteOriginalColumn(column)} onUndo={() => removeDeleted(deletedIndex)} /> ); })}
) : null} {fields.map((field, index) => ( handleTypeChange(index, value)} onKeyChange={(value) => handleKeyChange(index, value)} onAutoincrementChange={(value) => handleAutoincrementChange(index, value)} onRemove={() => remove(index)} /> ))}
); } function ActionButtonWithPopover({ content, children, }: { content?: string; children: React.ReactElement; }) { if (!content) { return children; } return ( {children} ); } function getDeleteDisabledMessage({ columnName, currentTtlColumn, indexedColumns, }: { columnName: string; currentTtlColumn?: string; indexedColumns: Set; }) { const messages: string[] = []; if (currentTtlColumn === columnName) { messages.push(i18n('tooltip_ttl-delete-disabled')); } if (indexedColumns.has(columnName)) { messages.push(i18n('tooltip_index-delete-disabled')); } return messages.join(' '); } function SpecialColumnLabel({label, columns}: {label: string; columns: string[]}) { if (!columns.length) { return null; } return (
{label}: {columns.join(', ')}
); } function formatDefaultValue(column: Column) { if (column.autoincrement) { return i18n('label_autoincrement'); } if (typeof column.defaultValue === 'undefined') { return ''; } return String(column.defaultValue); } function PrimaryColumnRow({ column, showDefaultColumn, }: { column: Column; showDefaultColumn: boolean; }) { return (
{column.name}
{column.type}
{column.notNull ? i18n('value_yes') : i18n('value_no')}
{showDefaultColumn ? (
{formatDefaultValue(column)}
) : null}
); } function NonPrimaryColumnRow({ column, showDefaultColumn, isDeleting, deleteDisabledMessage, onDelete, onUndo, }: { column: Column; showDefaultColumn: boolean; isDeleting: boolean; deleteDisabledMessage?: string; onDelete: () => void; onUndo: () => void; }) { return (
{column.name}
{column.type}
{column.notNull ? i18n('value_yes') : i18n('value_no')}
{showDefaultColumn ? (
{formatDefaultValue(column)}
) : null}
{isDeleting ? ( ) : ( )}
); } interface EditableColumnRowProps { index: number; mode: FormMode; showDefaultColumn: boolean; typeOptions: SelectOption[]; pkTypes: Set; keyNullable: boolean; onTypeChange: (value: string | undefined) => void; onKeyChange: (value: boolean) => void; onAutoincrementChange: (value: boolean) => void; onRemove: () => void; } function EditableColumnRow({ index, mode, showDefaultColumn, typeOptions, pkTypes, keyNullable, onTypeChange, onKeyChange, onAutoincrementChange, onRemove, }: EditableColumnRowProps) { const {control, clearErrors, formState, setValue, trigger} = useFormContext(); const column = useWatch({control, name: `columns.${index}`}); const defaultValueFieldName = `columns.${index}.defaultValue` as const; const revalidateColumns = React.useCallback(() => { trigger(['columns', 'settings']).catch(() => undefined); }, [trigger]); const handleDefaultValueUpdate = React.useCallback( (value: string) => { setValue(defaultValueFieldName, value, { shouldDirty: true, shouldValidate: false, }); clearErrors(defaultValueFieldName); }, [clearErrors, defaultValueFieldName, setValue], ); const columnErrors = formState.errors.columns?.[index] as | { name?: {message?: string}; type?: {message?: string}; defaultValue?: {message?: string}; } | undefined; const nameError = columnErrors?.name?.message; const typeError = columnErrors?.type?.message; const defaultValueError = columnErrors?.defaultValue?.message; const notNullDisabledMessage = getNotNullDisabledMessage(column, keyNullable); const autoincrementDisabledMessage = getAutoincrementDisabledMessage(column); const keyDisabled = !pkTypes.has(column.type); let defaultValueControl: React.ReactNode = null; if (mode === 'create' && showDefaultColumn) { if (column.key) { defaultValueControl = (
( { field.onChange(value); onAutoincrementChange(value); }} > {i18n('label_autoincrement')} )} />
); } else { defaultValueControl = (
( )} />
( { field.onBlur(); trigger(defaultValueFieldName).catch(() => undefined); }} disabled={!column.withDefaultValue} validationState={defaultValueError ? 'invalid' : undefined} /> )} />
{defaultValueError ? (
) : null}
); } } return (
( )} />
(