| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422 |
- import { PermissionAction } from '@supabase/shared-types/out/constants'
- import { useParams } from 'common'
- import { noop } from 'lodash'
- import {
- Braces,
- Calendar,
- DiamondIcon,
- Fingerprint,
- Hash,
- Key,
- Link as LinkIcon,
- ListPlus,
- MoreVertical,
- Plus,
- Search,
- ToggleRight,
- Trash,
- Type,
- } from 'lucide-react'
- import { useState } from 'react'
- import {
- Button,
- Card,
- cn,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuTrigger,
- Table,
- TableBody,
- TableCell,
- TableFooter,
- TableHead,
- TableHeader,
- TableRow,
- Tooltip,
- TooltipContent,
- TooltipTrigger,
- } from 'ui'
- import { Input } from 'ui-patterns/DataInputs/Input'
- import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
- import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning'
- import {
- getColumnTypeAffordance,
- getForeignKeyColumnNames,
- getPrimaryKeyColumnNames,
- getUniqueIndexColumnNames,
- } from './ColumnList.utils'
- import { ConstraintToken } from './ConstraintToken'
- import { displayColumnType } from '@/components/interfaces/TableGridEditor/SidePanelEditor/ColumnEditor/ColumnEditor.utils'
- import AlertError from '@/components/ui/AlertError'
- import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
- import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip'
- import { NoSearchResults } from '@/components/ui/NoSearchResults'
- import { useTableEditorQuery } from '@/data/table-editor/table-editor-query'
- import { isTableLike } from '@/data/table-editor/table-editor-types'
- import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
- import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
- import { useIsProtectedSchema } from '@/hooks/useProtectedSchemas'
- import type { SafePostgresColumn } from '@/lib/postgres-types'
- const getColumnTypeAffordancePresentation = (column: SafePostgresColumn) => {
- const { kind, label } = getColumnTypeAffordance(column.format)
- const iconClassName = 'text-foreground-muted'
- switch (kind) {
- case 'number':
- return {
- icon: <Hash size={14} className={iconClassName} strokeWidth={1.5} />,
- label,
- }
- case 'time':
- return {
- icon: <Calendar size={14} className={iconClassName} strokeWidth={1.5} />,
- label,
- }
- case 'text':
- return {
- icon: <Type size={14} className={iconClassName} strokeWidth={1.5} />,
- label,
- }
- case 'json':
- return {
- icon: <Braces size={14} className={iconClassName} strokeWidth={1.5} />,
- label,
- }
- case 'bool':
- return {
- icon: <ToggleRight size={14} className={iconClassName} strokeWidth={1.5} />,
- label,
- }
- default:
- return {
- icon: <ListPlus size={16} className={iconClassName} strokeWidth={1.5} />,
- label,
- }
- }
- }
- interface ColumnListProps {
- onAddColumn: () => void
- onEditColumn: (column: SafePostgresColumn) => void
- onDeleteColumn: (column: SafePostgresColumn) => void
- }
- export const ColumnList = ({
- onAddColumn = noop,
- onEditColumn = noop,
- onDeleteColumn = noop,
- }: ColumnListProps) => {
- const { id: _id } = useParams()
- const id = _id ? Number(_id) : undefined
- const { data: project } = useSelectedProjectQuery()
- const {
- data: selectedTable,
- error,
- isError,
- isPending: isLoading,
- isSuccess,
- } = useTableEditorQuery({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- id,
- })
- const [filterString, setFilterString] = useState<string>('')
- const isTableEntity = isTableLike(selectedTable)
- const tableConstraintSource = isTableEntity ? selectedTable : undefined
- const primaryKeyColumns = getPrimaryKeyColumnNames(tableConstraintSource)
- const foreignKeyColumns = getForeignKeyColumnNames(tableConstraintSource)
- const uniqueIndexColumns = getUniqueIndexColumnNames(tableConstraintSource)
- const columns =
- (filterString.length === 0
- ? (selectedTable?.columns ?? [])
- : selectedTable?.columns?.filter((column) =>
- column.name.toLowerCase().includes(filterString.toLowerCase())
- )) ?? []
- const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedTable?.schema ?? '' })
- const { can: canUpdateColumns } = useAsyncCheckPermissions(
- PermissionAction.TENANT_SQL_ADMIN_WRITE,
- 'columns'
- )
- const deleteColumnTooltipText = !canUpdateColumns
- ? 'Additional permissions required to delete column'
- : undefined
- return (
- <div className="space-y-4">
- <div className="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
- <div className="w-full lg:w-52">
- <Input
- size="tiny"
- placeholder="Filter columns"
- value={filterString}
- onChange={(e) => setFilterString(e.target.value)}
- icon={<Search />}
- />
- </div>
- {!isSchemaLocked && isTableEntity && (
- <ButtonTooltip
- icon={<Plus />}
- disabled={!canUpdateColumns}
- onClick={() => onAddColumn()}
- tooltip={{
- content: {
- side: 'bottom',
- text: !canUpdateColumns
- ? 'You need additional permissions to create columns'
- : undefined,
- },
- }}
- >
- New column
- </ButtonTooltip>
- )}
- </div>
- {isSchemaLocked && (
- <ProtectedSchemaWarning schema={selectedTable?.schema ?? ''} entity="columns" />
- )}
- <Card>
- {isLoading ? (
- <div className="p-4">
- <GenericSkeletonLoader />
- </div>
- ) : (
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead className="w-0 px-0!" />
- <TableHead
- className={cn(columns.length === 0 ? 'text-foreground-muted' : undefined)}
- >
- Name
- </TableHead>
- <TableHead className={columns.length === 0 ? 'text-foreground-muted' : undefined}>
- Type
- </TableHead>
- <TableHead className={columns.length === 0 ? 'text-foreground-muted' : undefined}>
- Constraints
- </TableHead>
- <TableHead />
- </TableRow>
- </TableHeader>
- <TableBody>
- {isError && (
- <TableRow className="[&>td]:hover:bg-inherit">
- <TableCell colSpan={5}>
- <AlertError
- error={error}
- subject={`Failed to retrieve columns for table "${selectedTable?.schema}.${selectedTable?.name}"`}
- />
- </TableCell>
- </TableRow>
- )}
- {isSuccess && columns.length === 0 && filterString.length > 0 && (
- <TableRow className="[&>td]:hover:bg-inherit">
- <TableCell colSpan={5}>
- <NoSearchResults
- withinTableCell
- searchString={filterString}
- onResetFilter={() => setFilterString('')}
- />
- </TableCell>
- </TableRow>
- )}
- {isSuccess && columns.length === 0 && filterString.length === 0 && (
- <TableRow className="[&>td]:hover:bg-inherit">
- <TableCell colSpan={5}>
- <p className="text-sm text-foreground">No columns created yet</p>
- <p className="text-sm text-foreground-light">
- There are no columns in "{selectedTable?.schema}.{selectedTable?.name}"
- </p>
- </TableCell>
- </TableRow>
- )}
- {isSuccess &&
- columns.map((column) => {
- const { icon: TypeIcon, label: typeLabel } =
- getColumnTypeAffordancePresentation(column)
- const constraintTokens = [
- primaryKeyColumns.has(column.name) ? (
- <ConstraintToken
- key="primary"
- icon={<Key size={12} strokeWidth={1.7} className="shrink-0" />}
- label="Primary"
- variant="primary"
- />
- ) : null,
- foreignKeyColumns.has(column.name) ? (
- <ConstraintToken
- key="foreign-key"
- icon={
- <LinkIcon
- size={12}
- strokeWidth={1.7}
- className="shrink-0 text-foreground-muted"
- />
- }
- label="Foreign key"
- />
- ) : null,
- column.is_unique || uniqueIndexColumns.has(column.name) ? (
- <ConstraintToken
- key="unique"
- icon={
- <Fingerprint
- size={12}
- strokeWidth={1.7}
- className="shrink-0 text-foreground-light"
- />
- }
- label="Unique"
- />
- ) : null,
- column.is_identity ? (
- <ConstraintToken
- key="identity"
- icon={
- <Hash
- size={12}
- strokeWidth={1.7}
- className="shrink-0 text-foreground-lighter"
- />
- }
- label="Identity"
- />
- ) : null,
- <ConstraintToken
- key="nullability"
- icon={
- <DiamondIcon
- size={12}
- strokeWidth={1.7}
- className="shrink-0"
- fill={column.is_nullable ? 'none' : 'currentColor'}
- />
- }
- label={column.is_nullable ? 'Nullable' : 'Non-nullable'}
- variant="secondary"
- />,
- ].filter(Boolean)
- return (
- <TableRow key={column.name}>
- <TableCell className="w-0 pl-5! pr-1!">
- <Tooltip>
- <TooltipTrigger asChild className="cursor-default" aria-label={typeLabel}>
- <div className="flex w-4 justify-center">{TypeIcon}</div>
- </TooltipTrigger>
- <TooltipContent side="bottom">
- <div className="flex flex-col">
- <span>{column.data_type}</span>
- {column.format !== column.data_type && (
- <span className="text-xs text-foreground-light">
- {displayColumnType(
- column.format,
- column.format_schema,
- column.data_type === 'ARRAY'
- )}
- </span>
- )}
- </div>
- </TooltipContent>
- </Tooltip>
- </TableCell>
- <TableCell className="max-w-[160px] sm:max-w-[280px]">
- <div className="flex min-w-0 flex-col">
- <p>{column.name}</p>
- {column.comment !== null ? (
- <span
- className="max-w-md truncate text-foreground-lighter"
- title={column.comment}
- >
- {column.comment}
- </span>
- ) : null}
- </div>
- </TableCell>
- <TableCell>
- <p className="text-foreground-lighter">
- {displayColumnType(
- column.format,
- column.format_schema,
- column.data_type === 'ARRAY'
- )}
- </p>
- </TableCell>
- <TableCell>
- <div className="flex flex-wrap gap-1.5">{constraintTokens}</div>
- </TableCell>
- <TableCell className="text-right">
- {!isSchemaLocked && isTableEntity && (
- <div className="flex justify-end gap-2">
- <ButtonTooltip
- type="default"
- disabled={!canUpdateColumns}
- onClick={() => onEditColumn(column)}
- tooltip={{
- content: {
- side: 'bottom',
- text: !canUpdateColumns
- ? 'Additional permissions required to edit column'
- : undefined,
- },
- }}
- >
- Edit
- </ButtonTooltip>
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button type="default" className="px-1" icon={<MoreVertical />} />
- </DropdownMenuTrigger>
- <DropdownMenuContent side="bottom" align="end" className="w-32">
- <DropdownMenuItemTooltip
- disabled={!canUpdateColumns}
- onClick={() => onDeleteColumn(column)}
- className="gap-x-2"
- tooltip={{
- content: {
- side: 'left',
- text: deleteColumnTooltipText,
- },
- }}
- >
- <Trash size={12} />
- <p>Delete column</p>
- </DropdownMenuItemTooltip>
- </DropdownMenuContent>
- </DropdownMenu>
- </div>
- )}
- </TableCell>
- </TableRow>
- )
- })}
- </TableBody>
- {isSuccess && (
- <TableFooter className="font-normal">
- <TableRow className="border-b-0 [&>td]:hover:bg-inherit">
- <TableCell colSpan={5} className="text-foreground-muted">
- {columns.length} {columns.length === 1 ? 'column' : 'columns'}
- </TableCell>
- </TableRow>
- </TableFooter>
- )}
- </Table>
- )}
- </Card>
- </div>
- )
- }
|