Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
291 changes: 172 additions & 119 deletions apps/studio/components/interfaces/Database/Tables/ColumnList.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { noop } from 'lodash'
import { Check, ChevronLeft, Edit, MoreVertical, Plus, Search, Trash, X } from 'lucide-react'
import Link from 'next/link'
import { useState } from 'react'

import { PostgresColumn } from '@supabase/postgres-meta'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { NoSearchResults } from 'components/ui/NoSearchResults'
Expand All @@ -15,18 +9,30 @@ 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 { noop } from 'lodash'
import { Check, Edit, MoreVertical, Plus, Search, Trash, X } from 'lucide-react'
import { useState } from 'react'
import {
Button,
Card,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Input,
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'

interface ColumnListProps {
Expand All @@ -40,7 +46,7 @@ export const ColumnList = ({
onEditColumn = noop,
onDeleteColumn = noop,
}: ColumnListProps) => {
const { id: _id, ref } = useParams()
const { id: _id } = useParams()
const id = _id ? Number(_id) : undefined

const { data: project } = useSelectedProjectQuery()
Expand All @@ -62,7 +68,9 @@ export const ColumnList = ({
const columns =
(filterString.length === 0
? (selectedTable?.columns ?? [])
: selectedTable?.columns?.filter((column) => column.name.includes(filterString))) ?? []
: selectedTable?.columns?.filter((column) =>
column.name.toLowerCase().includes(filterString.toLowerCase())
)) ?? []

const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedTable?.schema ?? '' })
const { can: canUpdateColumns } = useAsyncCheckPermissions(
Expand All @@ -72,16 +80,13 @@ export const ColumnList = ({

return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<Button asChild type="outline" icon={<ChevronLeft />} style={{ padding: '5px' }}>
<Link href={`/project/${ref}/database/tables`} />
</Button>
<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="small"
size="tiny"
placeholder="Filter columns"
value={filterString}
onChange={(e: any) => setFilterString(e.target.value)}
onChange={(e) => setFilterString(e.target.value)}
icon={<Search />}
/>
</div>
Expand All @@ -108,115 +113,163 @@ export const ColumnList = ({
<ProtectedSchemaWarning schema={selectedTable?.schema ?? ''} entity="columns" />
)}

{isLoading && <GenericSkeletonLoader />}
<Card>
{isLoading ? (
<div className="p-4">
<GenericSkeletonLoader />
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className={columns.length === 0 ? 'text-foreground-muted' : undefined}>
Name
</TableHead>
<TableHead className={columns.length === 0 ? 'text-foreground-muted' : undefined}>
Data Type
</TableHead>
<TableHead className={columns.length === 0 ? 'text-foreground-muted' : undefined}>
Format
</TableHead>
<TableHead
className={
columns.length === 0 ? 'text-right text-foreground-muted' : 'text-right'
}
>
Nullable
</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>
)}

{isError && (
<AlertError
error={error as any}
subject={`Failed to retrieve columns for table "${selectedTable?.schema}.${selectedTable?.name}"`}
/>
)}
{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 ? (
<NoSearchResults
searchString={filterString}
onResetFilter={() => setFilterString('')}
/>
) : (
<div>
<Table
head={[
<Table.th key="name">Name</Table.th>,
<Table.th key="description" className="hidden lg:table-cell">
Description
</Table.th>,
<Table.th key="type">Data Type</Table.th>,
<Table.th key="format">Format</Table.th>,
<Table.th key="format" className="text-center">
Nullable
</Table.th>,
<Table.th key="buttons"></Table.th>,
]}
body={columns.map((x) => (
<Table.tr className="border-t" key={x.name}>
<Table.td>
<p>{x.name}</p>
</Table.td>
<Table.td className="break-all whitespace-normal hidden xl:table-cell">
{x.comment !== null ? (
<p title={x.comment}>{x.comment}</p>
) : (
<p className="text-border-stronger">No description</p>
)}
</Table.td>
<Table.td>
<code className="text-code-inline">{x.data_type}</code>
</Table.td>
<Table.td className="font-mono text-xs">
<code className="text-code-inline">{x.format}</code>
</Table.td>
<Table.td className="font-mono text-xs">
{x.is_nullable ? (
<Check size={16} className="mx-auto" />
{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.length > 0 &&
columns.map((column) => (
<TableRow key={column.name}>
<TableCell>
<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>
<code className="text-code-inline">{column.data_type}</code>
</TableCell>
<TableCell className="font-mono text-xs">
<code className="text-code-inline">{column.format}</code>
</TableCell>
<TableCell className="text-right">
{column.is_nullable ? (
<div className="flex justify-end">
<Check size={16} strokeWidth={2} className="text-brand" />
</div>
) : (
<X size={16} className="mx-auto" />
<div className="flex justify-end">
<X size={16} strokeWidth={2} className="text-foreground-lighter" />
</div>
)}
</Table.td>
<Table.td className="text-right">
</TableCell>
<TableCell className="text-right">
{!isSchemaLocked && isTableEntity && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default" className="px-1" icon={<MoreVertical />} />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-32">
<Tooltip>
<TooltipTrigger>
<DropdownMenuItem
disabled={!canUpdateColumns}
onClick={() => onEditColumn(x)}
className="space-x-2"
>
<Edit size={12} />
<p>Edit column</p>
</DropdownMenuItem>
</TooltipTrigger>
{!canUpdateColumns && (
<TooltipContent side="bottom">
Additional permissions required to edit column
</TooltipContent>
)}
</Tooltip>

<Tooltip>
<TooltipTrigger>
<DropdownMenuItem
disabled={!canUpdateColumns || isSchemaLocked}
onClick={() => onDeleteColumn(x)}
className="space-x-2"
>
<Trash stroke="red" size={12} />
<p>Delete column</p>
</DropdownMenuItem>
</TooltipTrigger>
{!canUpdateColumns && (
<TooltipContent side="bottom">
Additional permissions required to delete column
</TooltipContent>
)}
</Tooltip>
</DropdownMenuContent>
</DropdownMenu>
<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">
<Tooltip>
<TooltipTrigger>
<DropdownMenuItem
disabled={!canUpdateColumns || isSchemaLocked}
onClick={() => onDeleteColumn(column)}
className="space-x-2"
>
<Trash size={12} />
<p>Delete column</p>
</DropdownMenuItem>
</TooltipTrigger>
{!canUpdateColumns && (
<TooltipContent side="bottom">
Additional permissions required to delete column
</TooltipContent>
)}
</Tooltip>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</Table.td>
</Table.tr>
</TableCell>
</TableRow>
))}
/>
</div>
)}
</>
)}
</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>
)
}
Loading
Loading