import SVG from 'react-inlinesvg' import { useMemo, useState } from 'react' import Link from 'next/link' import { noop, partition } from 'lodash' import { observer } from 'mobx-react-lite' import { Button, Dropdown, IconCheck, IconChevronDown, IconChevronsDown, IconCopy, IconEdit, IconLoader, IconLock, IconRefreshCw, IconSearch, IconTrash, IconX, Input, Listbox, Menu, } from 'ui' import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' import type { PostgresSchema } from '@supabase/postgres-meta' import { BASE_PATH } from 'lib/constants' import { checkPermissions, useStore, useParams, useLocalStorage } from 'hooks' import ProductMenuItem from 'components/ui/ProductMenu/ProductMenuItem' import { useEntityTypesQuery } from 'data/entity-types/entity-types-infinite-query' import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' import { Entity } from 'data/entity-types/entity-type-query' import { useProjectContext } from '../ProjectLayout/ProjectContext' import InfiniteList from 'components/ui/InfiniteList' import clsx from 'clsx' export interface TableEditorMenuProps { selectedSchema?: string onSelectSchema: (schema: string) => void onAddTable: () => void onEditTable: (table: Entity) => void onDeleteTable: (table: Entity) => void onDuplicateTable: (table: Entity) => void } const TableEditorMenu = ({ selectedSchema, onSelectSchema = noop, onAddTable = noop, onEditTable = noop, onDeleteTable = noop, onDuplicateTable = noop, }: TableEditorMenuProps) => { const { meta } = useStore() const { id } = useParams() const [searchText, setSearchText] = useState('') const [sort, setSort] = useLocalStorage<'alphabetical' | 'grouped-alphabetical'>( 'table-editor-sort', 'alphabetical' ) const { project } = useProjectContext() const { data, isLoading, refetch, isRefetching, hasNextPage, isFetchingNextPage, fetchNextPage, isPreviousData: isSearching, } = useEntityTypesQuery( { projectRef: project?.ref, connectionString: project?.connectionString, schema: selectedSchema, search: searchText || undefined, sort, }, { keepPreviousData: true, } ) const totalCount = data?.pages?.[0].data.count const entityTypes = useMemo( () => data?.pages.flatMap((page) => page.data.entities), [data?.pages] ) const schemas: PostgresSchema[] = meta.schemas.list() const schema = schemas.find((schema) => schema.name === selectedSchema) const canCreateTables = checkPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables') const isLoadingTableMetadata = id ? !meta.tables.byId(id) : true const refreshTables = async () => { await refetch() } const [protectedSchemas, openSchemas] = partition(schemas, (schema) => meta.excludedSchemas.includes(schema?.name ?? '') ) const isLocked = protectedSchemas.some((s) => s.id === schema?.id) return (
{/* Schema selection dropdown */}
{!meta.schemas.isInitialized ? (
Loading schemas...
) : ( { setSearchText('') onSelectSchema(name) }} >

Schemas

{openSchemas.map((schema) => ( schema} > {schema.name} ))}

Protected schemas

{protectedSchemas.map((schema) => ( schema} > {schema.name} ))}
)}
{!isLocked && (
{/* Add new table button */}
} type="default" style={{ justifyContent: 'start' }} onClick={onAddTable} > New table {!canCreateTables && (
You need additional permissions to create tables
)}
)} {/* Table search input */}
) : ( ) } placeholder="Search tables" onChange={(e) => setSearchText(e.target.value.trim())} value={searchText} size="tiny" actions={ searchText && ( ) } />
{isLoading ? (

Loading entities...

) : searchText.length === 0 && (entityTypes?.length ?? 0) === 0 ? (

No entities available

This schema has no entities available yet

) : searchText.length > 0 && (entityTypes?.length ?? 0) === 0 ? (

No results found

There are no entities that match your search

) : (

Tables

{totalCount !== undefined && (

({totalCount})

)}
) : (
) } onClick={() => { setSort('alphabetical') }} > Alphabetical , ) : (
) } onClick={() => { setSort('grouped-alphabetical') }} > Entity Type , ]} >
Sort By
} />
28} hasNextPage={hasNextPage} isLoadingNextPage={isFetchingNextPage} onLoadNextPage={() => fetchNextPage()} />
)} ) } export default observer(TableEditorMenu) export interface EntityListItemProps { id: number projectRef: string item: Entity isLocked: boolean onEditTable: (table: Entity) => void onDeleteTable: (table: Entity) => void onDuplicateTable: (table: Entity) => void isLoadingTableMetadata?: boolean } const EntityListItem = ({ id, projectRef, item: entity, isLocked, onEditTable, onDeleteTable, onDuplicateTable, isLoadingTableMetadata = false, }: EntityListItemProps) => { const isActive = Number(id) === entity.id const formatTooltipText = (entityType: string) => { return Object.entries(ENTITY_TYPE) .find(([, value]) => value === entityType)?.[0] ?.toLowerCase() ?.split('_') ?.join(' ') } return ( {entity.type === ENTITY_TYPE.TABLE ? ( code.replace(/svg/, 'svg class="m-auto text-color-inherit"') } /> ) : entity.type === ENTITY_TYPE.VIEW ? ( code.replace(/svg/, 'svg class="m-auto text-color-inherit"') } /> ) : (
{Object.entries(ENTITY_TYPE) .find(([, value]) => value === entity.type)?.[0]?.[0] ?.toUpperCase()}
)}
{formatTooltipText(entity.type)}
} action={ entity.type === ENTITY_TYPE.TABLE && isActive && !isLocked && ( } onClick={() => onEditTable(entity)} disabled={isLoadingTableMetadata} > Edit Table , } onClick={() => onDuplicateTable(entity)} disabled={isLoadingTableMetadata} > Duplicate Table , }> View Policies , , } onClick={() => onDeleteTable(entity)} disabled={isLoadingTableMetadata} > Delete Table , ]} >
) } /> ) }