Files
supabase/apps/studio/components/interfaces/Database/Indexes/CreateIndexSidePanel.tsx
Ivan Vasilov 56de26fe22 chore: Migrate the monorepo to use Tailwind v4 (#45318)
This PR migrates the whole monorepo to use Tailwind v4:
- Removed `@tailwindcss/container-queries` plugin since it's included by
default in v4,
- Bump all instances of Tailwind to v4. Made minimal changes to the
shared config to remove non-supported features (`alpha` mentions),
- Migrate all apps to be compatible with v4 configs,
- Fix the `typography.css` import in 3 apps,
- Add missing rules which were included by default in v3,
- Run `pnpm dlx @tailwindcss/upgrade` on all apps, which renames a lot
of classes
- Rename all misnamed classes according to
https://tailwindcss.com/docs/upgrade-guide#renamed-utilities in all
apps.

---------

Co-authored-by: Jordi Enric <jordi.err@gmail.com>
2026-04-30 10:53:24 +00:00

461 lines
17 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_Shadcn_,
CommandEmpty_Shadcn_,
CommandGroup_Shadcn_,
CommandInput_Shadcn_,
CommandItem_Shadcn_,
CommandList_Shadcn_,
Popover_Shadcn_,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectItem_Shadcn_,
SelectSeparator_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
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_Shadcn_
modal={false}
open={schemaDropdownOpen}
onOpenChange={setSchemaDropdownOpen}
>
<PopoverTrigger_Shadcn_ 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_Shadcn_>
<PopoverContent_Shadcn_
className="p-0"
side="bottom"
align="start"
sameWidthAsTrigger
>
<Command_Shadcn_>
<CommandInput_Shadcn_
placeholder="Find schema..."
value={schemaSearchTerm}
onValueChange={setSchemaSearchTerm}
/>
<CommandList_Shadcn_
className={cn((schemas ?? []).length > 7 && 'max-h-[210px]! overflow-y-auto')}
onWheel={(event) => event.stopPropagation()}
>
<CommandEmpty_Shadcn_>No schemas found</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_>
{(schemas ?? []).map((schema) => (
<CommandItem_Shadcn_
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_Shadcn_>
))}
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</Command_Shadcn_>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
</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_Shadcn_
modal={false}
open={tableDropdownOpen}
onOpenChange={setTableDropdownOpen}
>
<PopoverTrigger_Shadcn_
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_Shadcn_>
<PopoverContent_Shadcn_
className="p-0"
side="bottom"
align="start"
sameWidthAsTrigger
>
{/* [Terry] shouldFilter context:
https://github.com/pacocoursey/cmdk/issues/267#issuecomment-2252717107 */}
<Command_Shadcn_ shouldFilter={false}>
<CommandInput_Shadcn_
placeholder="Find table..."
value={searchTerm}
onValueChange={handleSearchChange}
/>
<CommandList_Shadcn_
className={cn(entityTypes.length > 7 && 'max-h-[210px]! overflow-y-auto')}
onWheel={(event) => event.stopPropagation()}
>
<CommandEmpty_Shadcn_>
{isLoadingEntities ? (
<div className="flex items-center gap-2 text-center justify-center">
<Loader2 size={12} className="animate-spin" />
Loading...
</div>
) : (
'No tables found'
)}
</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_>
{entityTypes.map((entity) => (
<CommandItem_Shadcn_
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_Shadcn_>
))}
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</Command_Shadcn_>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
</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_Shadcn_
disabled={isOrioleDb}
value={selectedIndexType}
onValueChange={setSelectedIndexType}
name="selected-index-type"
>
<SelectTrigger_Shadcn_ size={'small'}>
<SelectValue_Shadcn_ className="font-mono">
{selectedIndexType}
</SelectValue_Shadcn_>
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
{INDEX_TYPES.map((index, i) => (
<Fragment key={index.name}>
<SelectItem_Shadcn_ 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_Shadcn_>
{i < INDEX_TYPES.length - 1 && <SelectSeparator_Shadcn_ />}
</Fragment>
))}
</SelectContent_Shadcn_>
</Select_Shadcn_>
</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>
)
}