mirror of
https://github.com/supabase/supabase.git
synced 2026-05-08 09:50:33 -04:00
56de26fe22
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>
461 lines
17 KiB
TypeScript
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>
|
|
)
|
|
}
|