mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 18:35:07 +03:00
## Problem The `_Shadcn_` suffix isn't needed anymore on `Command` components ## Solution - Remove the `_Shadcn_` suffix - Simplify UI package exports - Apply prettier <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Simplified command component imports and exports across the UI library by removing internal naming aliases and adopting direct component references. Updated the public UI package barrel export to use wildcard re-exports for cleaner API surface. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46153?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
438 lines
16 KiB
TypeScript
438 lines
16 KiB
TypeScript
import { Check, ChevronsUpDown, Loader2 } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { Fragment, useEffect, useMemo, useState } from 'react'
|
|
import { toast } from 'sonner'
|
|
import {
|
|
Button,
|
|
cn,
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectSeparator,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
SidePanel,
|
|
} from 'ui'
|
|
import { Admonition } from 'ui-patterns'
|
|
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
|
import {
|
|
MultiSelector,
|
|
MultiSelectorContent,
|
|
MultiSelectorItem,
|
|
MultiSelectorList,
|
|
MultiSelectorTrigger,
|
|
} from 'ui-patterns/multi-select'
|
|
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
|
|
|
import { INDEX_TYPES } from './Indexes.constants'
|
|
import CodeEditor from '@/components/ui/CodeEditor/CodeEditor'
|
|
import { DocsButton } from '@/components/ui/DocsButton'
|
|
import { useDatabaseIndexCreateMutation } from '@/data/database-indexes/index-create-mutation'
|
|
import { useSchemasQuery } from '@/data/database/schemas-query'
|
|
import { useTableColumnsQuery } from '@/data/database/table-columns-query'
|
|
import { useEntityTypesQuery } from '@/data/entity-types/entity-types-infinite-query'
|
|
import { useIsOrioleDb, useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
import { DOCS_URL } from '@/lib/constants'
|
|
|
|
interface CreateIndexSidePanelProps {
|
|
visible: boolean
|
|
onClose: () => void
|
|
}
|
|
|
|
export const CreateIndexSidePanel = ({ visible, onClose }: CreateIndexSidePanelProps) => {
|
|
const { data: project } = useSelectedProjectQuery()
|
|
const isOrioleDb = useIsOrioleDb()
|
|
|
|
const [selectedSchema, setSelectedSchema] = useState('public')
|
|
const [selectedEntity, setSelectedEntity] = useState<string | undefined>(undefined)
|
|
const [selectedColumns, setSelectedColumns] = useState<string[]>([])
|
|
const [selectedIndexType, setSelectedIndexType] = useState<string>(INDEX_TYPES[0].value)
|
|
const [schemaDropdownOpen, setSchemaDropdownOpen] = useState(false)
|
|
const [tableDropdownOpen, setTableDropdownOpen] = useState(false)
|
|
const [schemaSearchTerm, setSchemaSearchTerm] = useState('')
|
|
const [searchTerm, setSearchTerm] = useState('')
|
|
|
|
const { data: schemas } = useSchemasQuery({
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
})
|
|
const { data: entities, isPending: isLoadingEntities } = useEntityTypesQuery({
|
|
schemas: [selectedSchema],
|
|
sort: 'alphabetical',
|
|
search: searchTerm,
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
})
|
|
const {
|
|
data: tableColumns,
|
|
isPending: isLoadingTableColumns,
|
|
isSuccess: isSuccessTableColumns,
|
|
} = useTableColumnsQuery({
|
|
schema: selectedSchema,
|
|
table: selectedEntity,
|
|
projectRef: project?.ref,
|
|
connectionString: project?.connectionString,
|
|
})
|
|
|
|
const { mutate: createIndex, isPending: isExecuting } = useDatabaseIndexCreateMutation({
|
|
onSuccess: () => {
|
|
onClose()
|
|
toast.success(`Successfully created index`)
|
|
},
|
|
})
|
|
|
|
const entityTypes = useMemo(
|
|
() => entities?.pages.flatMap((page) => page.data.entities) || [],
|
|
[entities?.pages]
|
|
)
|
|
function handleSearchChange(value: string) {
|
|
setSearchTerm(value)
|
|
}
|
|
|
|
function handleSchemaSelect(schema: string) {
|
|
setSelectedSchema(schema)
|
|
setSearchTerm('')
|
|
setSchemaSearchTerm('')
|
|
setSchemaDropdownOpen(false)
|
|
}
|
|
|
|
const columns = tableColumns?.[0]?.columns ?? []
|
|
const columnOptions = columns
|
|
.filter((column): column is NonNullable<typeof column> => column !== null)
|
|
.map((column) => ({
|
|
id: column.attname,
|
|
value: column.attname,
|
|
name: column.attname,
|
|
disabled: false,
|
|
}))
|
|
|
|
const generatedSQL = `
|
|
CREATE INDEX ON "${selectedSchema}"."${selectedEntity}" USING ${selectedIndexType} (${selectedColumns
|
|
.map((column) => `"${column}"`)
|
|
.join(', ')});
|
|
`.trim()
|
|
|
|
const onSaveIndex = () => {
|
|
if (!project) return console.error('Project is required')
|
|
if (!selectedEntity) return console.error('Entity is required')
|
|
|
|
createIndex({
|
|
projectRef: project.ref,
|
|
connectionString: project.connectionString,
|
|
payload: {
|
|
schema: selectedSchema,
|
|
entity: selectedEntity,
|
|
type: selectedIndexType,
|
|
columns: selectedColumns,
|
|
},
|
|
})
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (visible) {
|
|
setSelectedSchema('public')
|
|
setSelectedEntity('')
|
|
setSelectedColumns([])
|
|
setSelectedIndexType(INDEX_TYPES[0].value)
|
|
setSchemaSearchTerm('')
|
|
setSearchTerm('')
|
|
}
|
|
}, [visible])
|
|
|
|
useEffect(() => {
|
|
setSelectedEntity('')
|
|
setSelectedColumns([])
|
|
setSelectedIndexType(INDEX_TYPES[0].value)
|
|
setSearchTerm('')
|
|
}, [selectedSchema])
|
|
|
|
useEffect(() => {
|
|
setSelectedColumns([])
|
|
setSelectedIndexType(INDEX_TYPES[0].value)
|
|
}, [selectedEntity])
|
|
|
|
useEffect(() => {
|
|
if (!schemaDropdownOpen) setSchemaSearchTerm('')
|
|
}, [schemaDropdownOpen])
|
|
|
|
const isSelectEntityDisabled = entityTypes.length === 0 && searchTerm.trim().length === 0
|
|
|
|
return (
|
|
<SidePanel
|
|
size="large"
|
|
header="Create new index"
|
|
visible={visible}
|
|
onCancel={onClose}
|
|
onConfirm={() => onSaveIndex()}
|
|
loading={isExecuting}
|
|
confirmText="Create index"
|
|
>
|
|
<div className="py-6 space-y-6">
|
|
<SidePanel.Content className="space-y-6">
|
|
<FormItemLayout label="Select a schema" name="select-schema" isReactForm={false}>
|
|
<Popover modal={false} open={schemaDropdownOpen} onOpenChange={setSchemaDropdownOpen}>
|
|
<PopoverTrigger asChild>
|
|
<Button
|
|
type="default"
|
|
size={'medium'}
|
|
className={`w-full [&>span]:w-full text-left`}
|
|
iconRight={
|
|
<ChevronsUpDown className="text-foreground-muted" strokeWidth={2} size={14} />
|
|
}
|
|
>
|
|
{selectedSchema !== undefined && selectedSchema !== ''
|
|
? selectedSchema
|
|
: 'Choose a schema'}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0" side="bottom" align="start" sameWidthAsTrigger>
|
|
<Command>
|
|
<CommandInput
|
|
placeholder="Find schema..."
|
|
value={schemaSearchTerm}
|
|
onValueChange={setSchemaSearchTerm}
|
|
/>
|
|
<CommandList
|
|
className={cn((schemas ?? []).length > 7 && 'max-h-[210px]! overflow-y-auto')}
|
|
onWheel={(event) => event.stopPropagation()}
|
|
>
|
|
<CommandEmpty>No schemas found</CommandEmpty>
|
|
<CommandGroup>
|
|
{(schemas ?? []).map((schema) => (
|
|
<CommandItem
|
|
key={schema.name}
|
|
className="cursor-pointer flex items-center justify-between space-x-2 w-full"
|
|
onSelect={() => {
|
|
handleSchemaSelect(schema.name)
|
|
}}
|
|
onClick={() => {
|
|
handleSchemaSelect(schema.name)
|
|
}}
|
|
>
|
|
<span>{schema.name}</span>
|
|
{selectedSchema === schema.name && (
|
|
<Check className="text-brand" strokeWidth={2} size={16} />
|
|
)}
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</FormItemLayout>
|
|
|
|
<FormItemLayout
|
|
label="Select a table"
|
|
name="select-table"
|
|
description={
|
|
isSelectEntityDisabled &&
|
|
!isLoadingEntities &&
|
|
'Create a table in this schema via the Table or SQL editor first'
|
|
}
|
|
isReactForm={false}
|
|
>
|
|
<Popover modal={false} open={tableDropdownOpen} onOpenChange={setTableDropdownOpen}>
|
|
<PopoverTrigger asChild disabled={isSelectEntityDisabled || isLoadingEntities}>
|
|
<Button
|
|
type="default"
|
|
size="medium"
|
|
className={cn(
|
|
'w-full [&>span]:w-full text-left',
|
|
selectedEntity === '' && 'text-foreground-lighter'
|
|
)}
|
|
iconRight={
|
|
<ChevronsUpDown className="text-foreground-muted" strokeWidth={2} size={14} />
|
|
}
|
|
>
|
|
{selectedEntity !== undefined && selectedEntity !== ''
|
|
? selectedEntity
|
|
: isSelectEntityDisabled
|
|
? 'No tables available in schema'
|
|
: 'Choose a table'}
|
|
</Button>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="p-0" side="bottom" align="start" sameWidthAsTrigger>
|
|
{/* [Terry] shouldFilter context:
|
|
https://github.com/pacocoursey/cmdk/issues/267#issuecomment-2252717107 */}
|
|
<Command shouldFilter={false}>
|
|
<CommandInput
|
|
placeholder="Find table..."
|
|
value={searchTerm}
|
|
onValueChange={handleSearchChange}
|
|
/>
|
|
<CommandList
|
|
className={cn(entityTypes.length > 7 && 'max-h-[210px]! overflow-y-auto')}
|
|
onWheel={(event) => event.stopPropagation()}
|
|
>
|
|
<CommandEmpty>
|
|
{isLoadingEntities ? (
|
|
<div className="flex items-center gap-2 text-center justify-center">
|
|
<Loader2 size={12} className="animate-spin" />
|
|
Loading...
|
|
</div>
|
|
) : (
|
|
'No tables found'
|
|
)}
|
|
</CommandEmpty>
|
|
<CommandGroup>
|
|
{entityTypes.map((entity) => (
|
|
<CommandItem
|
|
key={entity.name}
|
|
className="cursor-pointer flex items-center justify-between space-x-2 w-full"
|
|
onSelect={() => {
|
|
setSelectedEntity(entity.name)
|
|
setTableDropdownOpen(false)
|
|
}}
|
|
onClick={() => {
|
|
setSelectedEntity(entity.name)
|
|
setTableDropdownOpen(false)
|
|
}}
|
|
>
|
|
<span>{entity.name}</span>
|
|
{selectedEntity === entity.name && (
|
|
<Check className="text-brand" strokeWidth={2} size={16} />
|
|
)}
|
|
</CommandItem>
|
|
))}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</Command>
|
|
</PopoverContent>
|
|
</Popover>
|
|
</FormItemLayout>
|
|
|
|
{selectedEntity && (
|
|
<FormItemLayout id="columns" label="Select up to 32 columns" isReactForm={false}>
|
|
{isLoadingTableColumns && <ShimmeringLoader className="py-4" />}
|
|
{isSuccessTableColumns && (
|
|
<MultiSelector values={selectedColumns} onValuesChange={setSelectedColumns}>
|
|
<MultiSelectorTrigger
|
|
id="columns"
|
|
mode="inline-combobox"
|
|
label={
|
|
selectedColumns.length === 0
|
|
? 'Choose which columns to create an index on'
|
|
: 'Search for a column'
|
|
}
|
|
deletableBadge
|
|
badgeLimit="wrap"
|
|
showIcon={false}
|
|
/>
|
|
<MultiSelectorContent>
|
|
<MultiSelectorList>
|
|
{columnOptions.map((option) => (
|
|
<MultiSelectorItem
|
|
key={option.id}
|
|
value={option.value}
|
|
disabled={option.disabled}
|
|
>
|
|
{option.name}
|
|
</MultiSelectorItem>
|
|
))}
|
|
</MultiSelectorList>
|
|
</MultiSelectorContent>
|
|
</MultiSelector>
|
|
)}
|
|
</FormItemLayout>
|
|
)}
|
|
</SidePanel.Content>
|
|
|
|
{selectedColumns.length > 0 && (
|
|
<>
|
|
<SidePanel.Separator />
|
|
<SidePanel.Content className="space-y-6">
|
|
<FormItemLayout
|
|
label="Select an index type"
|
|
name="selected-index-type"
|
|
isReactForm={false}
|
|
>
|
|
<Select
|
|
disabled={isOrioleDb}
|
|
value={selectedIndexType}
|
|
onValueChange={setSelectedIndexType}
|
|
name="selected-index-type"
|
|
>
|
|
<SelectTrigger size={'small'}>
|
|
<SelectValue className="font-mono">{selectedIndexType}</SelectValue>
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{INDEX_TYPES.map((index, i) => (
|
|
<Fragment key={index.name}>
|
|
<SelectItem value={index.value}>
|
|
<div className="flex flex-col gap-0.5">
|
|
<span>{index.name}</span>
|
|
{index.description.split('\n').map((x, idx) => (
|
|
<span
|
|
className="text-foreground-lighter group-focus:text-foreground-light group-data-checked:text-foreground-light"
|
|
key={`${index.value}-description-${idx}`}
|
|
>
|
|
{x}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</SelectItem>
|
|
{i < INDEX_TYPES.length - 1 && <SelectSeparator />}
|
|
</Fragment>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</FormItemLayout>
|
|
{isOrioleDb && (
|
|
<Admonition
|
|
type="default"
|
|
className="mt-2!"
|
|
title="OrioleDB currently only supports the B-tree index type"
|
|
description="More index types may be supported when OrioleDB is no longer in preview"
|
|
>
|
|
{/* [Joshen Oriole] Hook up proper docs URL */}
|
|
<DocsButton className="mt-2" abbrev={false} href={`${DOCS_URL}`} />
|
|
</Admonition>
|
|
)}
|
|
</SidePanel.Content>
|
|
<SidePanel.Separator />
|
|
<SidePanel.Content>
|
|
<div className="flex items-center justify-between">
|
|
<p className="text-sm">Preview of SQL statement</p>
|
|
<Button asChild type="default">
|
|
<Link
|
|
href={
|
|
project !== undefined
|
|
? `/project/${project.ref}/sql/new?content=${generatedSQL}`
|
|
: '/'
|
|
}
|
|
>
|
|
Open in SQL Editor
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
</SidePanel.Content>
|
|
<div className="h-[200px] mt-2!">
|
|
<div className="relative h-full">
|
|
<CodeEditor
|
|
isReadOnly
|
|
autofocus={false}
|
|
id={`${selectedSchema}-${selectedEntity}-${selectedColumns.join(
|
|
','
|
|
)}-${selectedIndexType}`}
|
|
language="pgsql"
|
|
defaultValue={generatedSQL}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</SidePanel>
|
|
)
|
|
}
|