mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 03:45:06 +03:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Refactor + bug fixes (part of the SafeSql migration stack — PR 2 of 7, stacks on top of #45897). ## What is the current behavior? - `pgMeta.columns.create` and the table-editor SQL builder take column type as a string with array suffix and schema baked in (e.g. `'private.test_enum'`, `'int4[]'`). - The studio table-editor SQL emits the legacy schema-embedded `format` string for enums in non-public schemas, while the pg-meta columns SQL already returns the new shape (bare `format` + separate `format_schema`). The two queries disagree on how to represent the same column, surfacing as a false-positive type mismatch in the FK selector when both ends are an enum from a non-public schema. - The FK selector compares column types by `format` alone — same-named enums in different schemas appear equal, and arrays vs. scalars of the same base type pass the family check. - `displayColumnType` renders arrays as the raw `_typname` pg-meta emits (e.g. `_int4` instead of `int4[]`). ## What is the new behavior? **pg-meta** - Introduce `ColumnTypeRef` (`{ schema?, name, isArray? }`) for column type input, replacing the legacy string-with-array-suffix format. `pgMeta.columns.create` and the table-editor SQL builder consume the new shape. - Add `format_schema` to the column zod schema; pg-meta SQL emits the type's schema for the table editor's ColumnType dropdown. - `pgMeta.columns.create` returns a `SafeSqlFragment`. - Studio table-editor SQL now emits bare `format` + `format_schema`, matching pg-meta's columns SQL. **Studio** - `SafePostgresColumn`/`SafePostgresTable` extend the new `PG*` types (master dropped postgres-meta). - Pipe `ColumnTypeRef` through `SidePanelEditor` → `ColumnEditor` → `TableEditor`, along with the column-create mutation, table retrieve/list queries, and the `TableList`/`ColumnList` surfaces. - `displayColumnType` helper renders arrays as `type[]` (or `schema.type[]`) and handles non-implicit schemas. - FK selector now carries `sourceIsArray`/`targetIsArray` and compares the full `(format, format_schema, isArray)` triple. Family checks for numeric/text/uuid skip when either side is an array (FKs across array boundaries are never compatible). - Type-mismatch and type-notice alerts pass `isArray` to the display helper. - Bundle `Policies.utils` + `Policies.types` + `sql-policy-mutation`, `PolicyEditorModal`, and `SchemaGraph` here because `SidePanelEditor` consumes `acceptGeneratedPolicy`/`AcceptedGeneratedPolicy` — splitting requires temporary overloads with no architectural payoff. ## Additional context Part of the SafeSql migration stack. Stacks on top of #45897. ### Manual test checklist Surfaces touched by this PR — please exercise each: **Table editor** - [x] Create a new table with a mix of column types (scalar, array, enum, foreign key) - [x] Add a column to an existing table; verify the type dropdown lists scalars + arrays separately and shows schema-qualified names for non-public enums - [x] Edit an existing column's type (scalar ↔ array, switch between enums in different schemas) and save - [x] Verify enum types from a non-public schema (e.g. `private.my_enum`) display as `private.my_enum` in the column list **Foreign key selector** - [x] Open the FK selector for a column and pick a target column with a matching type — no mismatch warning - [x] Pick a target column whose type differs only by schema (two same-named enums in different schemas) — should show a type-mismatch alert - [x] Pick a target column where one side is an array and the other is a scalar of the same base type — should show a type-mismatch alert (no auto-cast across array boundary) - [x] When FK target sets the column type, verify `format_schema` and `isArray` are preserved on the source column - [x] Type-mismatch and type-notice alert messages render array types as `type[]` (not `_type`) **Column list / table list** - [x] Schema-qualified type names display correctly for columns whose type lives in a non-public schema - [x] Array columns display as `type[]` (or `schema.type[]`) **Policies (bundled due to import dependency)** - [x] Open the Policies page; create/edit/delete a row-level policy via the modal - [x] Generate a policy via the AI assistant and accept it through `SidePanelEditor` — verify the accepted policy lands in the editor correctly **Schema visualizer** - [x] Open the Schemas → Schema Visualizer page; verify it renders without type errors and shows tables/relationships <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Improvements** * Support for column types in non-public schemas and richer column type presentation (includes schema and array info). * Stronger SQL safety around policies and constraints; draft policy SQL is now promoted explicitly on save. * Improved foreign-key type validation and compatibility checks using enhanced type metadata. * **Tests** * Updated snapshots and tests to reflect new column metadata and SQL fragment handling. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45903) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
600 lines
25 KiB
TypeScript
600 lines
25 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||
import { useParams } from 'common'
|
||
import { noop } from 'lodash'
|
||
import { Check, Copy, Edit, Eye, Filter, MoreVertical, Plus, Search, Trash, X } from 'lucide-react'
|
||
import Link from 'next/link'
|
||
import { useRouter } from 'next/router'
|
||
import { parseAsString, useQueryState } from 'nuqs'
|
||
import { useRef, useState } from 'react'
|
||
import {
|
||
Button,
|
||
Card,
|
||
Checkbox,
|
||
DropdownMenu,
|
||
DropdownMenuContent,
|
||
DropdownMenuItem,
|
||
DropdownMenuSeparator,
|
||
DropdownMenuTrigger,
|
||
Label_Shadcn_,
|
||
Popover_Shadcn_,
|
||
PopoverContent_Shadcn_,
|
||
PopoverTrigger_Shadcn_,
|
||
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 { formatAllEntities } from './Tables.utils'
|
||
import { buildTableEditorUrl } from '@/components/grid/SupabaseGrid.utils'
|
||
import AlertError from '@/components/ui/AlertError'
|
||
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
|
||
import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip'
|
||
import { EntityTypeIcon } from '@/components/ui/EntityTypeIcon'
|
||
import SchemaSelector from '@/components/ui/SchemaSelector'
|
||
import { Shortcut } from '@/components/ui/Shortcut'
|
||
import { useDatabasePublicationsQuery } from '@/data/database-publications/database-publications-query'
|
||
import { ENTITY_TYPE } from '@/data/entity-types/entity-type-constants'
|
||
import { useForeignTablesQuery } from '@/data/foreign-tables/foreign-tables-query'
|
||
import { useMaterializedViewsQuery } from '@/data/materialized-views/materialized-views-query'
|
||
import { usePrefetchEditorTablePage } from '@/data/prefetchers/project.$ref.editor.$id'
|
||
import { useTablesQuery } from '@/data/tables/tables-query'
|
||
import { useViewsQuery } from '@/data/views/views-query'
|
||
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
||
import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState'
|
||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||
import { useIsProtectedSchema } from '@/hooks/useProtectedSchemas'
|
||
import { onSearchInputEscape } from '@/lib/keyboard'
|
||
import type { SafePostgresTable } from '@/lib/postgres-types'
|
||
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
|
||
import { useShortcut } from '@/state/shortcuts/useShortcut'
|
||
|
||
interface TableListProps {
|
||
onAddTable: () => void
|
||
onEditTable: (table: SafePostgresTable) => void
|
||
onDeleteTable: (table: SafePostgresTable) => void
|
||
onDuplicateTable: (table: SafePostgresTable) => void
|
||
}
|
||
|
||
export const TableList = ({
|
||
onDuplicateTable,
|
||
onAddTable = noop,
|
||
onEditTable = noop,
|
||
onDeleteTable = noop,
|
||
}: TableListProps) => {
|
||
const router = useRouter()
|
||
const { ref } = useParams()
|
||
const { data: project } = useSelectedProjectQuery()
|
||
|
||
const prefetchEditorTablePage = usePrefetchEditorTablePage()
|
||
|
||
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
|
||
|
||
const [filterString, setFilterString] = useQueryState('search', parseAsString.withDefault(''))
|
||
const [visibleTypes, setVisibleTypes] = useState<string[]>(Object.values(ENTITY_TYPE))
|
||
const [schemaSelectorOpen, setSchemaSelectorOpen] = useState(false)
|
||
const searchInputRef = useRef<HTMLInputElement>(null)
|
||
|
||
const { can: canUpdateTables } = useAsyncCheckPermissions(
|
||
PermissionAction.TENANT_SQL_ADMIN_WRITE,
|
||
'tables'
|
||
)
|
||
|
||
const {
|
||
data: tables,
|
||
error: tablesError,
|
||
isError: isErrorTables,
|
||
isPending: isLoadingTables,
|
||
isSuccess: isSuccessTables,
|
||
} = useTablesQuery(
|
||
{
|
||
projectRef: project?.ref,
|
||
connectionString: project?.connectionString,
|
||
schema: selectedSchema,
|
||
sortByProperty: 'name',
|
||
includeColumns: true,
|
||
},
|
||
{
|
||
select(tables) {
|
||
return filterString.length === 0
|
||
? tables
|
||
: tables.filter((table) => table.name.toLowerCase().includes(filterString.toLowerCase()))
|
||
},
|
||
}
|
||
)
|
||
|
||
const {
|
||
data: views,
|
||
error: viewsError,
|
||
isError: isErrorViews,
|
||
isPending: isLoadingViews,
|
||
isSuccess: isSuccessViews,
|
||
} = useViewsQuery(
|
||
{
|
||
projectRef: project?.ref,
|
||
connectionString: project?.connectionString,
|
||
schema: selectedSchema,
|
||
},
|
||
{
|
||
select(views) {
|
||
return filterString.length === 0
|
||
? views
|
||
: views.filter((view) => view.name.toLowerCase().includes(filterString.toLowerCase()))
|
||
},
|
||
}
|
||
)
|
||
|
||
const {
|
||
data: materializedViews,
|
||
error: materializedViewsError,
|
||
isError: isErrorMaterializedViews,
|
||
isPending: isLoadingMaterializedViews,
|
||
isSuccess: isSuccessMaterializedViews,
|
||
} = useMaterializedViewsQuery(
|
||
{
|
||
projectRef: project?.ref,
|
||
connectionString: project?.connectionString,
|
||
schema: selectedSchema,
|
||
},
|
||
{
|
||
select(materializedViews) {
|
||
return filterString.length === 0
|
||
? materializedViews
|
||
: materializedViews.filter((view) =>
|
||
view.name.toLowerCase().includes(filterString.toLowerCase())
|
||
)
|
||
},
|
||
}
|
||
)
|
||
|
||
const {
|
||
data: foreignTables,
|
||
error: foreignTablesError,
|
||
isError: isErrorForeignTables,
|
||
isPending: isLoadingForeignTables,
|
||
isSuccess: isSuccessForeignTables,
|
||
} = useForeignTablesQuery(
|
||
{
|
||
projectRef: project?.ref,
|
||
connectionString: project?.connectionString,
|
||
schema: selectedSchema,
|
||
},
|
||
{
|
||
select(foreignTables) {
|
||
return filterString.length === 0
|
||
? foreignTables
|
||
: foreignTables.filter((table) =>
|
||
table.name.toLowerCase().includes(filterString.toLowerCase())
|
||
)
|
||
},
|
||
}
|
||
)
|
||
|
||
const { data: publications } = useDatabasePublicationsQuery({
|
||
projectRef: project?.ref,
|
||
connectionString: project?.connectionString,
|
||
})
|
||
const realtimePublication = (publications ?? []).find(
|
||
(publication) => publication.name === 'supabase_realtime'
|
||
)
|
||
|
||
const entities = formatAllEntities({ tables, views, materializedViews, foreignTables }).filter(
|
||
(x) => visibleTypes.includes(x.type)
|
||
)
|
||
|
||
const { isSchemaLocked } = useIsProtectedSchema({ schema: selectedSchema })
|
||
|
||
const canAddTables = canUpdateTables && !isSchemaLocked
|
||
|
||
useShortcut(
|
||
SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH,
|
||
() => {
|
||
searchInputRef.current?.focus()
|
||
searchInputRef.current?.select()
|
||
},
|
||
{ label: 'Search tables' }
|
||
)
|
||
|
||
useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => {
|
||
setVisibleTypes(Object.values(ENTITY_TYPE))
|
||
setFilterString('')
|
||
})
|
||
|
||
const error = tablesError || viewsError || materializedViewsError || foreignTablesError
|
||
const isError = isErrorTables || isErrorViews || isErrorMaterializedViews || isErrorForeignTables
|
||
const isLoading =
|
||
isLoadingTables || isLoadingViews || isLoadingMaterializedViews || isLoadingForeignTables
|
||
const isSuccess =
|
||
isSuccessTables && isSuccessViews && isSuccessMaterializedViews && isSuccessForeignTables
|
||
|
||
const formatTooltipText = (entityType: string) => {
|
||
const text =
|
||
Object.entries(ENTITY_TYPE)
|
||
.find(([, value]) => value === entityType)?.[0]
|
||
?.toLowerCase()
|
||
?.split('_')
|
||
?.join(' ') || ''
|
||
// Return sentence case (capitalize first letter only)
|
||
return text.charAt(0).toUpperCase() + text.slice(1)
|
||
}
|
||
|
||
return (
|
||
<div className="flex flex-col gap-y-4">
|
||
<div className="flex flex-col lg:flex-row lg:items-center gap-2 flex-wrap">
|
||
<div className="flex gap-2 items-center">
|
||
<Shortcut
|
||
id={SHORTCUT_IDS.LIST_PAGE_FOCUS_SCHEMA}
|
||
onTrigger={() => setSchemaSelectorOpen(true)}
|
||
side="bottom"
|
||
tooltipOpen={schemaSelectorOpen ? false : undefined}
|
||
>
|
||
<SchemaSelector
|
||
className="grow lg:grow-0 w-[180px]"
|
||
size="tiny"
|
||
showError={false}
|
||
selectedSchemaName={selectedSchema}
|
||
onSelectSchema={setSelectedSchema}
|
||
open={schemaSelectorOpen}
|
||
onOpenChange={setSchemaSelectorOpen}
|
||
/>
|
||
</Shortcut>
|
||
<Popover_Shadcn_>
|
||
<PopoverTrigger_Shadcn_ asChild>
|
||
<Button
|
||
size="tiny"
|
||
type={visibleTypes.length !== 5 ? 'default' : 'dashed'}
|
||
className="px-1"
|
||
icon={<Filter />}
|
||
/>
|
||
</PopoverTrigger_Shadcn_>
|
||
<PopoverContent_Shadcn_ className="p-0 w-56" side="bottom" align="center">
|
||
<div className="px-3 pt-3 pb-2 flex flex-col gap-y-2">
|
||
<p className="text-xs">Show entity types</p>
|
||
<div className="flex flex-col">
|
||
{Object.entries(ENTITY_TYPE).map(([key, value]) => (
|
||
<div key={key} className="group flex items-center justify-between py-0.5">
|
||
<div className="flex items-center gap-x-2">
|
||
<Checkbox
|
||
id={key}
|
||
name={key}
|
||
checked={visibleTypes.includes(value)}
|
||
onCheckedChange={() => {
|
||
if (visibleTypes.includes(value)) {
|
||
setVisibleTypes(visibleTypes.filter((y) => y !== value))
|
||
} else {
|
||
setVisibleTypes(visibleTypes.concat([value]))
|
||
}
|
||
}}
|
||
/>
|
||
<Label_Shadcn_ htmlFor={key} className="capitalize text-xs">
|
||
{key.toLowerCase().replace('_', ' ')}
|
||
</Label_Shadcn_>
|
||
</div>
|
||
<Button
|
||
size="tiny"
|
||
type="default"
|
||
onClick={() => setVisibleTypes([value])}
|
||
className="transition opacity-0 group-hover:opacity-100 h-auto px-1 py-0.5"
|
||
>
|
||
Select only
|
||
</Button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</PopoverContent_Shadcn_>
|
||
</Popover_Shadcn_>
|
||
</div>
|
||
<div className="flex grow justify-between gap-2 items-center">
|
||
<Input
|
||
ref={searchInputRef}
|
||
size="tiny"
|
||
containerClassName="grow lg:grow-0 w-52"
|
||
placeholder="Search for a table"
|
||
value={filterString}
|
||
onChange={(e) => setFilterString(e.target.value)}
|
||
onKeyDown={onSearchInputEscape(filterString, setFilterString)}
|
||
icon={<Search />}
|
||
/>
|
||
|
||
{!isSchemaLocked &&
|
||
(canAddTables ? (
|
||
<Shortcut
|
||
id={SHORTCUT_IDS.LIST_PAGE_NEW_ITEM}
|
||
label="Create new table"
|
||
onTrigger={() => onAddTable()}
|
||
side="bottom"
|
||
>
|
||
<Button className="w-auto ml-auto" icon={<Plus />} onClick={() => onAddTable()}>
|
||
New table
|
||
</Button>
|
||
</Shortcut>
|
||
) : (
|
||
<ButtonTooltip
|
||
className="w-auto ml-auto"
|
||
icon={<Plus />}
|
||
disabled
|
||
tooltip={{
|
||
content: {
|
||
side: 'bottom',
|
||
text: 'You need additional permissions to create tables',
|
||
},
|
||
}}
|
||
>
|
||
New table
|
||
</ButtonTooltip>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{isSchemaLocked && <ProtectedSchemaWarning schema={selectedSchema} entity="tables" />}
|
||
|
||
{isLoading && <GenericSkeletonLoader />}
|
||
|
||
{isError && <AlertError error={error} subject="Failed to retrieve tables" />}
|
||
|
||
{isSuccess && (
|
||
<div className="w-full">
|
||
<Card>
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead key="icon" className="w-0 px-0!" />
|
||
<TableHead key="name" className="max-w-[160px] sm:max-w-[280px]">
|
||
Name
|
||
</TableHead>
|
||
<TableHead key="columns">Columns</TableHead>
|
||
<TableHead key="rows">Rows (Estimated)</TableHead>
|
||
<TableHead key="size">Size (Estimated)</TableHead>
|
||
<TableHead key="realtime">Realtime</TableHead>
|
||
<TableHead key="buttons"></TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
<>
|
||
{entities.length === 0 && filterString.length === 0 && (
|
||
<TableRow key={selectedSchema}>
|
||
<TableCell colSpan={7}>
|
||
{visibleTypes.length === 0 ? (
|
||
<>
|
||
<p className="text-sm text-foreground">
|
||
Please select at least one entity type to filter with
|
||
</p>
|
||
<p className="text-sm text-foreground-light">
|
||
There are currently no results based on the filter that you have
|
||
applied
|
||
</p>
|
||
</>
|
||
) : (
|
||
<>
|
||
<p className="text-sm text-foreground">No tables created yet</p>
|
||
<p className="text-sm text-foreground-light">
|
||
There are no{' '}
|
||
{visibleTypes.length === 5
|
||
? 'tables'
|
||
: visibleTypes.length === 1
|
||
? `${formatTooltipText(visibleTypes[0])}s`
|
||
: `${visibleTypes
|
||
.slice(0, -1)
|
||
.map((x) => `${formatTooltipText(x)}s`)
|
||
.join(
|
||
', '
|
||
)}, and ${formatTooltipText(visibleTypes[visibleTypes.length - 1])}s`}{' '}
|
||
found in the schema "{selectedSchema}"
|
||
</p>
|
||
</>
|
||
)}
|
||
</TableCell>
|
||
</TableRow>
|
||
)}
|
||
{entities.length === 0 && filterString.length > 0 && (
|
||
<TableRow key={selectedSchema}>
|
||
<TableCell colSpan={7}>
|
||
<p className="text-sm text-foreground">No results found</p>
|
||
<p className="text-sm text-foreground-light">
|
||
Your search for "{filterString}" did not return any results
|
||
</p>
|
||
</TableCell>
|
||
</TableRow>
|
||
)}
|
||
{entities.length > 0 &&
|
||
entities.map((x) => (
|
||
<TableRow key={x.id}>
|
||
<TableCell className="w-0 pl-5! pr-1!">
|
||
<Tooltip>
|
||
<TooltipTrigger asChild className="cursor-default">
|
||
<div className="flex w-4 justify-center">
|
||
<EntityTypeIcon type={x.type} />
|
||
</div>
|
||
</TooltipTrigger>
|
||
<TooltipContent side="bottom">
|
||
{formatTooltipText(x.type)}
|
||
</TooltipContent>
|
||
</Tooltip>
|
||
</TableCell>
|
||
<TableCell className="max-w-[160px] sm:max-w-[280px]">
|
||
<div className="flex min-w-0 flex-col">
|
||
{/* only show tooltips if required, to reduce noise */}
|
||
{x.name.length > 20 ? (
|
||
<Tooltip disableHoverableContent={true}>
|
||
<TooltipTrigger
|
||
asChild
|
||
className="max-w-[95%] overflow-hidden text-ellipsis whitespace-nowrap"
|
||
>
|
||
<p>{x.name}</p>
|
||
</TooltipTrigger>
|
||
|
||
<TooltipContent side="bottom">{x.name}</TooltipContent>
|
||
</Tooltip>
|
||
) : (
|
||
<p>{x.name}</p>
|
||
)}
|
||
{x.comment !== null ? (
|
||
<span
|
||
className="max-w-md truncate text-foreground-lighter"
|
||
title={x.comment}
|
||
>
|
||
{x.comment}
|
||
</span>
|
||
) : null}
|
||
</div>
|
||
</TableCell>
|
||
<TableCell>
|
||
<p className="text-foreground-light">
|
||
{x.columns.length.toLocaleString()}
|
||
</p>
|
||
</TableCell>
|
||
<TableCell>
|
||
{x.rows !== undefined ? (
|
||
<p className="text-foreground-light">{x.rows.toLocaleString()}</p>
|
||
) : (
|
||
<p className="text-foreground-muted">–</p>
|
||
)}
|
||
</TableCell>
|
||
<TableCell>
|
||
{x.size !== undefined ? (
|
||
<p className="text-foreground-light">{x.size}</p>
|
||
) : (
|
||
<p className="text-foreground-muted">–</p>
|
||
)}
|
||
</TableCell>
|
||
<TableCell>
|
||
{(realtimePublication?.tables ?? []).find(
|
||
(table) => table.id === x.id
|
||
) ? (
|
||
<div className="flex items-center gap-x-2">
|
||
<Check size={16} strokeWidth={2} className="text-brand-link" />
|
||
<p className="text-foreground-light">Enabled</p>
|
||
</div>
|
||
) : (
|
||
<div className="flex items-center gap-x-2">
|
||
<X size={16} strokeWidth={2} className="text-foreground-muted" />
|
||
<p className="text-foreground-lighter">Disabled</p>
|
||
</div>
|
||
)}
|
||
</TableCell>
|
||
<TableCell>
|
||
<div className="flex justify-end gap-2">
|
||
<Button asChild type="default">
|
||
<Link href={`/project/${ref}/database/tables/${x.id}`}>
|
||
View columns
|
||
</Link>
|
||
</Button>
|
||
|
||
{!isSchemaLocked && (
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button type="default" className="px-1" icon={<MoreVertical />} />
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent side="bottom" align="end" className="w-40">
|
||
<DropdownMenuItem
|
||
className="flex items-center space-x-2"
|
||
onClick={() =>
|
||
router.push(
|
||
buildTableEditorUrl({
|
||
projectRef: project?.ref,
|
||
tableId: x.id,
|
||
schema: x.schema,
|
||
})
|
||
)
|
||
}
|
||
onMouseEnter={() =>
|
||
prefetchEditorTablePage({
|
||
id: x.id ? String(x.id) : undefined,
|
||
})
|
||
}
|
||
>
|
||
<Eye size={12} />
|
||
<p>View in Table Editor</p>
|
||
</DropdownMenuItem>
|
||
|
||
{x.type === ENTITY_TYPE.TABLE && (
|
||
<>
|
||
<DropdownMenuSeparator />
|
||
<DropdownMenuItemTooltip
|
||
className="gap-x-2"
|
||
disabled={!canUpdateTables}
|
||
onClick={() => {
|
||
if (canUpdateTables) onEditTable(x)
|
||
}}
|
||
tooltip={{
|
||
content: {
|
||
side: 'left',
|
||
text: 'You need additional permissions to edit this table',
|
||
},
|
||
}}
|
||
>
|
||
<Edit size={12} />
|
||
<p>Edit table</p>
|
||
</DropdownMenuItemTooltip>
|
||
<DropdownMenuItemTooltip
|
||
key="duplicate-table"
|
||
className="gap-x-2"
|
||
disabled={!canUpdateTables}
|
||
onClick={() => {
|
||
if (canUpdateTables) onDuplicateTable(x)
|
||
}}
|
||
tooltip={{
|
||
content: {
|
||
side: 'left',
|
||
text: 'You need additional permissions to duplicate tables',
|
||
},
|
||
}}
|
||
>
|
||
<Copy size={12} />
|
||
<span>Duplicate Table</span>
|
||
</DropdownMenuItemTooltip>
|
||
<DropdownMenuSeparator />
|
||
<DropdownMenuItemTooltip
|
||
disabled={!canUpdateTables || isSchemaLocked}
|
||
className="gap-x-2"
|
||
onClick={() => {
|
||
if (canUpdateTables && !isSchemaLocked) {
|
||
onDeleteTable({ ...x, schema: selectedSchema })
|
||
}
|
||
}}
|
||
tooltip={{
|
||
content: {
|
||
side: 'left',
|
||
text: 'You need additional permissions to delete tables',
|
||
},
|
||
}}
|
||
>
|
||
<Trash size={12} />
|
||
<p>Delete table</p>
|
||
</DropdownMenuItemTooltip>
|
||
</>
|
||
)}
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
)}
|
||
</div>
|
||
</TableCell>
|
||
</TableRow>
|
||
))}
|
||
</>
|
||
</TableBody>
|
||
<TableFooter className="font-normal">
|
||
<TableRow className="border-b-0 [&>td]:hover:bg-inherit">
|
||
<TableCell colSpan={7} className="text-foreground-muted">
|
||
{entities.length} {entities.length === 1 ? 'table' : 'tables'}
|
||
</TableCell>
|
||
</TableRow>
|
||
</TableFooter>
|
||
</Table>
|
||
</Card>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|