mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Chore: add param routing on panels (#40184)
* chore(studio): add useQueryStateWithSelect hook to handle nested param routing on panels * handleErrorOnDelete fn for useQueryStateWithSelect
This commit is contained in:
1 parent
35fc021028
commit
cb045c9559
11 files changed
+351
-184
No files matched your search
+3
@@ -8,12 +8,14 @@ interface DeleteEnumeratedTypeModalProps {
|
||||
visible: boolean
|
||||
selectedEnumeratedType?: any
|
||||
onClose: () => void
|
||||
onDelete?: () => void
|
||||
}
|
||||
|
||||
const DeleteEnumeratedTypeModal = ({
|
||||
visible,
|
||||
selectedEnumeratedType,
|
||||
onClose,
|
||||
onDelete,
|
||||
}: DeleteEnumeratedTypeModalProps) => {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { mutate: deleteEnumeratedType, isLoading: isDeleting } = useEnumeratedTypeDeleteMutation({
|
||||
@@ -29,6 +31,7 @@ const DeleteEnumeratedTypeModal = ({
|
||||
if (project?.connectionString === undefined)
|
||||
return console.error('Project connectionString required')
|
||||
|
||||
onDelete?.()
|
||||
deleteEnumeratedType({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Edit, MoreVertical, Search, Trash } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs'
|
||||
import { useRef, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { DocsButton } from 'components/ui/DocsButton'
|
||||
import SchemaSelector from 'components/ui/SchemaSelector'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import {
|
||||
EnumeratedType,
|
||||
useEnumeratedTypesQuery,
|
||||
} from 'data/enumerated-types/enumerated-types-query'
|
||||
import { useEnumeratedTypesQuery } from 'data/enumerated-types/enumerated-types-query'
|
||||
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { handleErrorOnDelete, useQueryStateWithSelect } from 'hooks/misc/useQueryStateWithSelect'
|
||||
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
|
||||
import {
|
||||
Button,
|
||||
@@ -36,14 +36,33 @@ export const EnumeratedTypes = () => {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const [search, setSearch] = useState('')
|
||||
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
|
||||
const [showCreateTypePanel, setShowCreateTypePanel] = useState(false)
|
||||
const [selectedTypeToEdit, setSelectedTypeToEdit] = useState<EnumeratedType>()
|
||||
const [selectedTypeToDelete, setSelectedTypeToDelete] = useState<EnumeratedType>()
|
||||
const deletingTypeIdRef = useRef<string | null>(null)
|
||||
|
||||
const { data, error, isLoading, isError, isSuccess } = useEnumeratedTypesQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
|
||||
const [showCreateTypePanel, setShowCreateTypePanel] = useQueryState(
|
||||
'new',
|
||||
parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
|
||||
)
|
||||
|
||||
const { value: typeToEdit, setValue: setSelectedTypeIdToEdit } = useQueryStateWithSelect({
|
||||
urlKey: 'edit',
|
||||
select: (id) => (id ? data?.find((type) => type.id.toString() === id) : undefined),
|
||||
enabled: !!data,
|
||||
onError: () => toast.error(`Enumerated Type not found`),
|
||||
})
|
||||
|
||||
const { value: typeToDelete, setValue: setSelectedTypeIdToDelete } = useQueryStateWithSelect({
|
||||
urlKey: 'delete',
|
||||
select: (id) => (id ? data?.find((type) => type.id.toString() === id) : undefined),
|
||||
enabled: !!data,
|
||||
onError: (_error, selectedId) =>
|
||||
handleErrorOnDelete(deletingTypeIdRef, selectedId, `Enumerated Type not found`),
|
||||
})
|
||||
|
||||
const enumeratedTypes = (data ?? []).filter((type) => type.enums.length > 0)
|
||||
const filteredEnumeratedTypes =
|
||||
search.length > 0
|
||||
@@ -150,14 +169,14 @@ export const EnumeratedTypes = () => {
|
||||
<DropdownMenuContent side="bottom" align="end" className="w-32">
|
||||
<DropdownMenuItem
|
||||
className="space-x-2"
|
||||
onClick={() => setSelectedTypeToEdit(type)}
|
||||
onClick={() => setSelectedTypeIdToEdit(type.id.toString())}
|
||||
>
|
||||
<Edit size={14} />
|
||||
<p>Update type</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="space-x-2"
|
||||
onClick={() => setSelectedTypeToDelete(type)}
|
||||
onClick={() => setSelectedTypeIdToDelete(type.id.toString())}
|
||||
>
|
||||
<Trash size={14} />
|
||||
<p>Delete type</p>
|
||||
@@ -182,15 +201,20 @@ export const EnumeratedTypes = () => {
|
||||
/>
|
||||
|
||||
<EditEnumeratedTypeSidePanel
|
||||
visible={selectedTypeToEdit !== undefined}
|
||||
selectedEnumeratedType={selectedTypeToEdit}
|
||||
onClose={() => setSelectedTypeToEdit(undefined)}
|
||||
visible={!!typeToEdit}
|
||||
selectedEnumeratedType={typeToEdit}
|
||||
onClose={() => setSelectedTypeIdToEdit(null)}
|
||||
/>
|
||||
|
||||
<DeleteEnumeratedTypeModal
|
||||
visible={selectedTypeToDelete !== undefined}
|
||||
selectedEnumeratedType={selectedTypeToDelete}
|
||||
onClose={() => setSelectedTypeToDelete(undefined)}
|
||||
visible={!!typeToDelete}
|
||||
selectedEnumeratedType={typeToDelete}
|
||||
onClose={() => setSelectedTypeIdToDelete(null)}
|
||||
onDelete={() => {
|
||||
if (typeToDelete) {
|
||||
deletingTypeIdRef.current = typeToDelete.id.toString()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,32 +1,34 @@
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { useDatabaseFunctionDeleteMutation } from 'data/database-functions/database-functions-delete-mutation'
|
||||
import { DatabaseFunction } from 'data/database-functions/database-functions-query'
|
||||
import type { DatabaseFunction } from 'data/database-functions/database-functions-query'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal'
|
||||
|
||||
interface DeleteFunctionProps {
|
||||
func?: DatabaseFunction
|
||||
visible: boolean
|
||||
setVisible: (value: boolean) => void
|
||||
setVisible: (value: string | null) => void
|
||||
onDelete: (params: {
|
||||
func: DatabaseFunction
|
||||
projectRef: string
|
||||
connectionString?: string | null
|
||||
}) => void
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
export const DeleteFunction = ({ func, visible, setVisible }: DeleteFunctionProps) => {
|
||||
export const DeleteFunction = ({
|
||||
func,
|
||||
visible,
|
||||
setVisible,
|
||||
onDelete,
|
||||
isLoading,
|
||||
}: DeleteFunctionProps) => {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { name, schema } = func ?? {}
|
||||
|
||||
const { mutate: deleteDatabaseFunction, isLoading } = useDatabaseFunctionDeleteMutation({
|
||||
onSuccess: () => {
|
||||
toast.success(`Successfully removed function ${name}`)
|
||||
setVisible(false)
|
||||
},
|
||||
})
|
||||
|
||||
async function handleDelete() {
|
||||
if (!func) return console.error('Function is required')
|
||||
if (!project) return console.error('Project is required')
|
||||
|
||||
deleteDatabaseFunction({
|
||||
onDelete({
|
||||
func,
|
||||
projectRef: project.ref,
|
||||
connectionString: project.connectionString,
|
||||
@@ -38,7 +40,7 @@ export const DeleteFunction = ({ func, visible, setVisible }: DeleteFunctionProp
|
||||
<TextConfirmModal
|
||||
variant={'warning'}
|
||||
visible={visible}
|
||||
onCancel={() => setVisible(!visible)}
|
||||
onCancel={() => setVisible(null)}
|
||||
onConfirm={handleDelete}
|
||||
title="Delete this function"
|
||||
loading={isLoading}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from 'ui'
|
||||
import type { DatabaseFunction } from 'data/database-functions/database-functions-query'
|
||||
|
||||
interface FunctionListProps {
|
||||
schema: string
|
||||
@@ -31,6 +32,7 @@ interface FunctionListProps {
|
||||
duplicateFunction: (fn: any) => void
|
||||
editFunction: (fn: any) => void
|
||||
deleteFunction: (fn: any) => void
|
||||
functions: DatabaseFunction[]
|
||||
}
|
||||
|
||||
const FunctionList = ({
|
||||
@@ -42,17 +44,13 @@ const FunctionList = ({
|
||||
duplicateFunction = noop,
|
||||
editFunction = noop,
|
||||
deleteFunction = noop,
|
||||
functions,
|
||||
}: FunctionListProps) => {
|
||||
const router = useRouter()
|
||||
const { data: selectedProject } = useSelectedProjectQuery()
|
||||
const aiSnap = useAiAssistantStateSnapshot()
|
||||
const { openSidebar } = useSidebarManagerSnapshot()
|
||||
|
||||
const { data: functions } = useDatabaseFunctionsQuery({
|
||||
projectRef: selectedProject?.ref,
|
||||
connectionString: selectedProject?.connectionString,
|
||||
})
|
||||
|
||||
const filteredFunctions = (functions ?? []).filter((x) => {
|
||||
const matchesName = includes(x.name.toLowerCase(), filterString.toLowerCase())
|
||||
const matchesReturnType =
|
||||
|
||||
+147
-16
@@ -1,9 +1,9 @@
|
||||
import type { PostgresFunction } from '@supabase/postgres-meta'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { noop } from 'lodash'
|
||||
import { Search } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { parseAsJson, useQueryState } from 'nuqs'
|
||||
import { parseAsBoolean, parseAsJson, useQueryState } from 'nuqs'
|
||||
import { useRef } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
|
||||
@@ -13,10 +13,12 @@ import SchemaSelector from 'components/ui/SchemaSelector'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
|
||||
import { useDatabaseFunctionsQuery } from 'data/database-functions/database-functions-query'
|
||||
import { useDatabaseFunctionDeleteMutation } from 'data/database-functions/database-functions-delete-mutation'
|
||||
import { useSchemasQuery } from 'data/database/schemas-query'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { handleErrorOnDelete, useQueryStateWithSelect } from 'hooks/misc/useQueryStateWithSelect'
|
||||
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
|
||||
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
|
||||
import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state'
|
||||
@@ -36,26 +38,80 @@ import {
|
||||
} from 'components/interfaces/Reports/v2/ReportsSelectFilter'
|
||||
import { ProtectedSchemaWarning } from '../../ProtectedSchemaWarning'
|
||||
import FunctionList from './FunctionList'
|
||||
import type { DatabaseFunction } from 'data/database-functions/database-functions-query'
|
||||
|
||||
interface FunctionsListProps {
|
||||
createFunction: () => void
|
||||
duplicateFunction: (fn: PostgresFunction) => void
|
||||
editFunction: (fn: PostgresFunction) => void
|
||||
deleteFunction: (fn: PostgresFunction) => void
|
||||
}
|
||||
import { useIsInlineEditorEnabled } from 'components/interfaces/Account/Preferences/InlineEditorSettings'
|
||||
import { CreateFunction } from 'components/interfaces/Database/Functions/CreateFunction'
|
||||
import { DeleteFunction } from 'components/interfaces/Database/Functions/DeleteFunction'
|
||||
import { useEditorPanelStateSnapshot } from 'state/editor-panel-state'
|
||||
|
||||
const FunctionsList = ({
|
||||
createFunction = noop,
|
||||
editFunction = noop,
|
||||
deleteFunction = noop,
|
||||
duplicateFunction = noop,
|
||||
}: FunctionsListProps) => {
|
||||
const createFunctionSnippet = `create function function_name()
|
||||
returns void
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
-- Write your function logic here
|
||||
end;
|
||||
$$;`
|
||||
|
||||
const FunctionsList = () => {
|
||||
const router = useRouter()
|
||||
const { search } = useParams()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const aiSnap = useAiAssistantStateSnapshot()
|
||||
const { openSidebar } = useSidebarManagerSnapshot()
|
||||
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
|
||||
const isInlineEditorEnabled = useIsInlineEditorEnabled()
|
||||
const {
|
||||
setValue: setEditorPanelValue,
|
||||
setTemplates: setEditorPanelTemplates,
|
||||
setInitialPrompt: setEditorPanelInitialPrompt,
|
||||
} = useEditorPanelStateSnapshot()
|
||||
|
||||
// Track the ID being deleted to exclude it from error checking
|
||||
const deletingFunctionIdRef = useRef<string | null>(null)
|
||||
|
||||
const createFunction = () => {
|
||||
setSelectedFunctionIdToDuplicate(null)
|
||||
if (isInlineEditorEnabled) {
|
||||
setEditorPanelInitialPrompt('Create a new database function that...')
|
||||
setEditorPanelValue(createFunctionSnippet)
|
||||
setEditorPanelTemplates([])
|
||||
openSidebar(SIDEBAR_KEYS.EDITOR_PANEL)
|
||||
} else {
|
||||
setShowCreateFunctionForm(true)
|
||||
}
|
||||
}
|
||||
|
||||
const duplicateFunction = (fn: DatabaseFunction) => {
|
||||
if (isInlineEditorEnabled) {
|
||||
const dupFn = {
|
||||
...fn,
|
||||
name: `${fn.name}_duplicate`,
|
||||
}
|
||||
setEditorPanelInitialPrompt('Create new database function that...')
|
||||
setEditorPanelValue(dupFn.complete_statement)
|
||||
setEditorPanelTemplates([])
|
||||
openSidebar(SIDEBAR_KEYS.EDITOR_PANEL)
|
||||
} else {
|
||||
setSelectedFunctionIdToDuplicate(fn.id.toString())
|
||||
}
|
||||
}
|
||||
|
||||
const editFunction = (fn: DatabaseFunction) => {
|
||||
setSelectedFunctionIdToDuplicate(null)
|
||||
if (isInlineEditorEnabled) {
|
||||
setEditorPanelValue(fn.complete_statement)
|
||||
setEditorPanelTemplates([])
|
||||
openSidebar(SIDEBAR_KEYS.EDITOR_PANEL)
|
||||
} else {
|
||||
setSelectedFunctionToEdit(fn.id.toString())
|
||||
}
|
||||
}
|
||||
|
||||
const deleteFunction = (fn: DatabaseFunction) => {
|
||||
setSelectedFunctionToDelete(fn.id.toString())
|
||||
}
|
||||
|
||||
const filterString = search ?? ''
|
||||
|
||||
@@ -114,12 +170,56 @@ const FunctionsList = ({
|
||||
...(hasInvoker ? [{ label: 'Invoker', value: 'invoker' }] : []),
|
||||
]
|
||||
|
||||
const [showCreateFunctionForm, setShowCreateFunctionForm] = useQueryState(
|
||||
'new',
|
||||
parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
|
||||
)
|
||||
|
||||
const { setValue: setSelectedFunctionToEdit, value: functionToEdit } = useQueryStateWithSelect({
|
||||
urlKey: 'edit',
|
||||
select: (id: string) => (id ? functions?.find((fn) => fn.id.toString() === id) : undefined),
|
||||
enabled: !!functions,
|
||||
onError: () => toast.error(`Function not found`),
|
||||
})
|
||||
|
||||
const { setValue: setSelectedFunctionIdToDuplicate, value: functionToDuplicate } =
|
||||
useQueryStateWithSelect({
|
||||
urlKey: 'duplicate',
|
||||
select: (id: string) => {
|
||||
if (!id) return undefined
|
||||
const original = functions?.find((fn) => fn.id.toString() === id)
|
||||
return original ? { ...original, name: `${original.name}_duplicate` } : undefined
|
||||
},
|
||||
enabled: !!functions,
|
||||
onError: () => toast.error(`Function not found`),
|
||||
})
|
||||
|
||||
const { setValue: setSelectedFunctionToDelete, value: functionToDelete } =
|
||||
useQueryStateWithSelect({
|
||||
urlKey: 'delete',
|
||||
select: (id: string) => (id ? functions?.find((fn) => fn.id.toString() === id) : undefined),
|
||||
enabled: !!functions,
|
||||
onError: (_error, selectedId) =>
|
||||
handleErrorOnDelete(deletingFunctionIdRef, selectedId, `Function not found`),
|
||||
})
|
||||
|
||||
const { mutate: deleteDatabaseFunction, isLoading: isDeletingFunction } =
|
||||
useDatabaseFunctionDeleteMutation({
|
||||
onSuccess: (_, variables) => {
|
||||
toast.success(`Successfully removed function ${variables.func.name}`)
|
||||
setSelectedFunctionToDelete(null)
|
||||
},
|
||||
onError: () => {
|
||||
deletingFunctionIdRef.current = null
|
||||
},
|
||||
})
|
||||
|
||||
if (isLoading) return <GenericSkeletonLoader />
|
||||
if (isError) return <AlertError error={error} subject="Failed to retrieve database functions" />
|
||||
|
||||
return (
|
||||
<>
|
||||
{(functions ?? []).length == 0 ? (
|
||||
{(functions ?? []).length === 0 ? (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<ProductEmptyState
|
||||
title="Functions"
|
||||
@@ -252,12 +352,43 @@ const FunctionsList = ({
|
||||
duplicateFunction={duplicateFunction}
|
||||
editFunction={editFunction}
|
||||
deleteFunction={deleteFunction}
|
||||
functions={functions ?? []}
|
||||
/>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create Function */}
|
||||
<CreateFunction
|
||||
visible={showCreateFunctionForm}
|
||||
onClose={() => {
|
||||
setShowCreateFunctionForm(false)
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Edit or Duplicate Function */}
|
||||
<CreateFunction
|
||||
func={functionToEdit || functionToDuplicate}
|
||||
visible={!!functionToEdit || !!functionToDuplicate}
|
||||
onClose={() => {
|
||||
setSelectedFunctionToEdit(null)
|
||||
setSelectedFunctionIdToDuplicate(null)
|
||||
}}
|
||||
isDuplicating={!!functionToDuplicate}
|
||||
/>
|
||||
|
||||
<DeleteFunction
|
||||
func={functionToDelete}
|
||||
visible={!!functionToDelete}
|
||||
setVisible={setSelectedFunctionToDelete}
|
||||
onDelete={(params: Parameters<typeof deleteDatabaseFunction>[0]) => {
|
||||
deletingFunctionIdRef.current = params.func.id.toString()
|
||||
deleteDatabaseFunction(params)
|
||||
}}
|
||||
isLoading={isDeletingFunction}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { sortBy } from 'lodash'
|
||||
import { AlertCircle, Search, Trash } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { useParams } from 'common'
|
||||
@@ -9,10 +10,11 @@ import CodeEditor from 'components/ui/CodeEditor/CodeEditor'
|
||||
import SchemaSelector from 'components/ui/SchemaSelector'
|
||||
import ShimmeringLoader, { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { useDatabaseIndexDeleteMutation } from 'data/database-indexes/index-delete-mutation'
|
||||
import { DatabaseIndex, useIndexesQuery } from 'data/database-indexes/indexes-query'
|
||||
import { useIndexesQuery, type DatabaseIndex } from 'data/database-indexes/indexes-query'
|
||||
import { useSchemasQuery } from 'data/database/schemas-query'
|
||||
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { handleErrorOnDelete, useQueryStateWithSelect } from 'hooks/misc/useQueryStateWithSelect'
|
||||
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
|
||||
import {
|
||||
Button,
|
||||
@@ -36,9 +38,7 @@ const Indexes = () => {
|
||||
|
||||
const [search, setSearch] = useState('')
|
||||
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
|
||||
const [showCreateIndex, setShowCreateIndex] = useState(false)
|
||||
const [selectedIndex, setSelectedIndex] = useState<DatabaseIndex>()
|
||||
const [selectedIndexToDelete, setSelectedIndexToDelete] = useState<DatabaseIndex>()
|
||||
const deletingIndexNameRef = useRef<string | null>(null)
|
||||
|
||||
const {
|
||||
data: allIndexes,
|
||||
@@ -51,6 +51,28 @@ const Indexes = () => {
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
|
||||
const [showCreateIndex, setShowCreateIndex] = useQueryState(
|
||||
'new',
|
||||
parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
|
||||
)
|
||||
|
||||
const { setValue: setSelectedIndexName, value: selectedIndex } = useQueryStateWithSelect({
|
||||
urlKey: 'edit',
|
||||
select: (id) => (id ? allIndexes?.find((idx) => idx.name === id) : undefined),
|
||||
enabled: !!allIndexes,
|
||||
onError: () => toast.error(`Index not found`),
|
||||
})
|
||||
|
||||
const { setValue: setSelectedIndexNameToDelete, value: selectedIndexToDelete } =
|
||||
useQueryStateWithSelect({
|
||||
urlKey: 'delete',
|
||||
select: (id) => (id ? allIndexes?.find((idx) => idx.name === id) : undefined),
|
||||
enabled: !!allIndexes,
|
||||
onError: (_error, selectedId) =>
|
||||
handleErrorOnDelete(deletingIndexNameRef, selectedId, `Index not found`),
|
||||
})
|
||||
|
||||
const {
|
||||
data: schemas,
|
||||
isLoading: isLoadingSchemas,
|
||||
@@ -63,7 +85,7 @@ const Indexes = () => {
|
||||
|
||||
const { mutate: deleteIndex, isLoading: isExecuting } = useDatabaseIndexDeleteMutation({
|
||||
onSuccess: async () => {
|
||||
setSelectedIndexToDelete(undefined)
|
||||
setSelectedIndexNameToDelete(null)
|
||||
toast.success('Successfully deleted index')
|
||||
},
|
||||
})
|
||||
@@ -79,6 +101,7 @@ const Indexes = () => {
|
||||
const onConfirmDeleteIndex = (index: DatabaseIndex) => {
|
||||
if (!project) return console.error('Project is required')
|
||||
|
||||
deletingIndexNameRef.current = index.name
|
||||
deleteIndex({
|
||||
projectRef: project.ref,
|
||||
connectionString: project.connectionString,
|
||||
@@ -195,7 +218,10 @@ const Indexes = () => {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex justify-end items-center space-x-2">
|
||||
<Button type="default" onClick={() => setSelectedIndex(index)}>
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() => setSelectedIndexName(index.name)}
|
||||
>
|
||||
View definition
|
||||
</Button>
|
||||
{!isSchemaLocked && (
|
||||
@@ -204,7 +230,7 @@ const Indexes = () => {
|
||||
type="text"
|
||||
className="px-1"
|
||||
icon={<Trash />}
|
||||
onClick={() => setSelectedIndexToDelete(index)}
|
||||
onClick={() => setSelectedIndexNameToDelete(index.name)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -221,14 +247,14 @@ const Indexes = () => {
|
||||
|
||||
<SidePanel
|
||||
size="xlarge"
|
||||
visible={selectedIndex !== undefined}
|
||||
visible={!!selectedIndex}
|
||||
header={
|
||||
<>
|
||||
<span>Index:</span>
|
||||
<code className="text-sm ml-2">{selectedIndex?.name}</code>
|
||||
</>
|
||||
}
|
||||
onCancel={() => setSelectedIndex(undefined)}
|
||||
onCancel={() => setSelectedIndexName(null)}
|
||||
>
|
||||
<div className="h-full">
|
||||
<div className="relative h-full">
|
||||
@@ -248,7 +274,7 @@ const Indexes = () => {
|
||||
variant="warning"
|
||||
size="medium"
|
||||
loading={isExecuting}
|
||||
visible={selectedIndexToDelete !== undefined}
|
||||
visible={!!selectedIndexToDelete}
|
||||
title={
|
||||
<>
|
||||
Confirm to delete index <code className="text-sm">{selectedIndexToDelete?.name}</code>
|
||||
@@ -259,7 +285,7 @@ const Indexes = () => {
|
||||
onConfirm={() =>
|
||||
selectedIndexToDelete !== undefined ? onConfirmDeleteIndex(selectedIndexToDelete) : {}
|
||||
}
|
||||
onCancel={() => setSelectedIndexToDelete(undefined)}
|
||||
onCancel={() => setSelectedIndexNameToDelete(null)}
|
||||
alert={{
|
||||
title: 'This action cannot be undone',
|
||||
description:
|
||||
|
||||
@@ -9,9 +9,10 @@ interface DeleteRoleModalProps {
|
||||
role: PostgresRole
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
onDelete?: () => void
|
||||
}
|
||||
|
||||
export const DeleteRoleModal = ({ role, visible, onClose }: DeleteRoleModalProps) => {
|
||||
export const DeleteRoleModal = ({ role, visible, onClose, onDelete }: DeleteRoleModalProps) => {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
|
||||
const { mutate: deleteDatabaseRole, isLoading: isDeleting } = useDatabaseRoleDeleteMutation({
|
||||
@@ -24,6 +25,7 @@ export const DeleteRoleModal = ({ role, visible, onClose }: DeleteRoleModalProps
|
||||
const deleteRole = async () => {
|
||||
if (!project) return console.error('Project is required')
|
||||
if (!role) return console.error('Failed to delete role: role is missing')
|
||||
onDelete?.()
|
||||
deleteDatabaseRole({
|
||||
projectRef: project.ref,
|
||||
connectionString: project.connectionString,
|
||||
|
||||
@@ -24,7 +24,7 @@ import { ROLE_PERMISSIONS } from './Roles.constants'
|
||||
interface RoleRowProps {
|
||||
role: PgRole
|
||||
disabled?: boolean
|
||||
onSelectDelete: (role: PgRole) => void
|
||||
onSelectDelete: (role: string) => void
|
||||
}
|
||||
|
||||
export const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps) => {
|
||||
@@ -141,7 +141,7 @@ export const RoleRow = ({ role, disabled = false, onSelectDelete }: RoleRowProps
|
||||
className="space-x-2"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onSelectDelete(role)
|
||||
onSelectDelete(role.id.toString())
|
||||
}}
|
||||
>
|
||||
<Trash className="text-red-800" size="14" strokeWidth={2} />
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { partition, sortBy } from 'lodash'
|
||||
import { Plus, Search, X } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs'
|
||||
import { useRef, useState } from 'react'
|
||||
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import NoSearchResults from 'components/ui/NoSearchResults'
|
||||
@@ -10,12 +11,14 @@ import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query'
|
||||
import { useMaxConnectionsQuery } from 'data/database/max-connections-query'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import { handleErrorOnDelete, useQueryStateWithSelect } from 'hooks/misc/useQueryStateWithSelect'
|
||||
import { Badge, Button, Input, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
import { CreateRolePanel } from './CreateRolePanel'
|
||||
import { DeleteRoleModal } from './DeleteRoleModal'
|
||||
import { RoleRow } from './RoleRow'
|
||||
import { RoleRowSkeleton } from './RoleRowSkeleton'
|
||||
import { SUPABASE_ROLES } from './Roles.constants'
|
||||
import type { PostgresRole } from '@supabase/postgres-meta'
|
||||
|
||||
type SUPABASE_ROLE = (typeof SUPABASE_ROLES)[number]
|
||||
|
||||
@@ -24,8 +27,7 @@ export const RolesList = () => {
|
||||
|
||||
const [filterString, setFilterString] = useState('')
|
||||
const [filterType, setFilterType] = useState<'all' | 'active'>('all')
|
||||
const [isCreatingRole, setIsCreatingRole] = useState(false)
|
||||
const [selectedRoleToDelete, setSelectedRoleToDelete] = useState<any>()
|
||||
const deletingRoleIdRef = useRef<string | null>(null)
|
||||
|
||||
const { can: canUpdateRoles } = useAsyncCheckPermissions(
|
||||
PermissionAction.TENANT_SQL_ADMIN_WRITE,
|
||||
@@ -42,6 +44,20 @@ export const RolesList = () => {
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
})
|
||||
|
||||
const [isCreatingRole, setIsCreatingRole] = useQueryState(
|
||||
'new',
|
||||
parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
|
||||
)
|
||||
|
||||
const { setValue: setSelectedRoleIdToDelete, value: roleToDelete } = useQueryStateWithSelect({
|
||||
urlKey: 'delete',
|
||||
select: (id: string) => (id ? data?.find((role) => role.id.toString() === id) : undefined),
|
||||
enabled: !!data,
|
||||
onError: (_error, selectedId) =>
|
||||
handleErrorOnDelete(deletingRoleIdRef, selectedId, `Database Role not found`),
|
||||
})
|
||||
|
||||
const roles = sortBy(data ?? [], (r) => r.name.toLocaleLowerCase())
|
||||
|
||||
const filteredRoles = (
|
||||
@@ -182,7 +198,7 @@ export const RolesList = () => {
|
||||
disabled
|
||||
key={role.id}
|
||||
role={role}
|
||||
onSelectDelete={setSelectedRoleToDelete}
|
||||
onSelectDelete={setSelectedRoleIdToDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -199,7 +215,7 @@ export const RolesList = () => {
|
||||
key={role.id}
|
||||
disabled={!canUpdateRoles}
|
||||
role={role}
|
||||
onSelectDelete={setSelectedRoleToDelete}
|
||||
onSelectDelete={setSelectedRoleIdToDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -212,9 +228,14 @@ export const RolesList = () => {
|
||||
<CreateRolePanel visible={isCreatingRole} onClose={() => setIsCreatingRole(false)} />
|
||||
|
||||
<DeleteRoleModal
|
||||
role={selectedRoleToDelete}
|
||||
visible={selectedRoleToDelete !== undefined}
|
||||
onClose={() => setSelectedRoleToDelete(undefined)}
|
||||
role={roleToDelete as unknown as PostgresRole}
|
||||
visible={!!roleToDelete}
|
||||
onClose={() => setSelectedRoleIdToDelete(null)}
|
||||
onDelete={() => {
|
||||
if (roleToDelete) {
|
||||
deletingRoleIdRef.current = roleToDelete.id.toString()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { MutableRefObject, useEffect, useMemo } from 'react'
|
||||
import { parseAsString, useQueryState } from 'nuqs'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
/**
|
||||
* Hook for managing URL query parameters with a custom select function and error handling.
|
||||
*
|
||||
* @param enabled - Whether error handling is active (shows error when selectedId exists but select returns undefined)
|
||||
* @param urlKey - The query parameter key (e.g., 'edit', 'delete')
|
||||
* @param select - Function to transform the selected ID into the desired value (returns undefined if not found)
|
||||
* @param onError - Callback invoked when enabled is true and selectedId exists but select returns undefined
|
||||
*
|
||||
* @returns Object with:
|
||||
* - value: The result of select(selectedId) or undefined
|
||||
* - setValue: Function to set/clear the selected ID in the URL
|
||||
*/
|
||||
export function useQueryStateWithSelect<T>({
|
||||
enabled,
|
||||
urlKey,
|
||||
select,
|
||||
onError,
|
||||
}: {
|
||||
enabled: boolean
|
||||
urlKey: string
|
||||
select: (id: string) => T | undefined
|
||||
onError: (error: Error, selectedId: string) => void
|
||||
}) {
|
||||
const [selectedId, setSelectedId] = useQueryState(
|
||||
urlKey,
|
||||
parseAsString.withOptions({ history: 'push', clearOnDefault: true })
|
||||
)
|
||||
|
||||
const value = useMemo(() => (selectedId ? select(selectedId) : undefined), [selectedId, select])
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled && selectedId && !value) {
|
||||
onError(new Error(`not found`), selectedId)
|
||||
setSelectedId(null)
|
||||
}
|
||||
}, [enabled, onError, selectedId, setSelectedId, value])
|
||||
|
||||
return {
|
||||
value,
|
||||
setValue: setSelectedId as (value: string | null) => void,
|
||||
}
|
||||
}
|
||||
|
||||
export const handleErrorOnDelete = (
|
||||
deletingIdRef: MutableRefObject<string | null>,
|
||||
selectedId: string,
|
||||
errorMessage: string
|
||||
) => {
|
||||
if (selectedId !== deletingIdRef.current) {
|
||||
toast.error(errorMessage)
|
||||
} else {
|
||||
deletingIdRef.current = null
|
||||
}
|
||||
}
|
||||
@@ -1,135 +1,37 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { useIsInlineEditorEnabled } from 'components/interfaces/Account/Preferences/InlineEditorSettings'
|
||||
import { CreateFunction } from 'components/interfaces/Database/Functions/CreateFunction'
|
||||
import { DeleteFunction } from 'components/interfaces/Database/Functions/DeleteFunction'
|
||||
import FunctionsList from 'components/interfaces/Database/Functions/FunctionsList/FunctionsList'
|
||||
import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
|
||||
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
|
||||
import { FormHeader } from 'components/ui/Forms/FormHeader'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
import { DatabaseFunction } from 'data/database-functions/database-functions-query'
|
||||
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
||||
import { DOCS_URL } from 'lib/constants'
|
||||
import { useEditorPanelStateSnapshot } from 'state/editor-panel-state'
|
||||
import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
|
||||
const DatabaseFunctionsPage: NextPageWithLayout = () => {
|
||||
const isInlineEditorEnabled = useIsInlineEditorEnabled()
|
||||
const { openSidebar } = useSidebarManagerSnapshot()
|
||||
const {
|
||||
setValue: setEditorPanelValue,
|
||||
setTemplates: setEditorPanelTemplates,
|
||||
setInitialPrompt: setEditorPanelInitialPrompt,
|
||||
} = useEditorPanelStateSnapshot()
|
||||
|
||||
const { can: canReadFunctions, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
|
||||
PermissionAction.TENANT_SQL_ADMIN_READ,
|
||||
'functions'
|
||||
)
|
||||
|
||||
const [isDuplicating, setIsDuplicating] = useState(false)
|
||||
const [selectedFunction, setSelectedFunction] = useState<DatabaseFunction | undefined>()
|
||||
const [showCreateFunctionForm, setShowCreateFunctionForm] = useState(false)
|
||||
const [showDeleteFunctionForm, setShowDeleteFunctionForm] = useState(false)
|
||||
|
||||
const createFunction = () => {
|
||||
setIsDuplicating(false)
|
||||
if (isInlineEditorEnabled) {
|
||||
setEditorPanelInitialPrompt('Create a new database function that...')
|
||||
setEditorPanelValue(`create function function_name()
|
||||
returns void
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
-- Write your function logic here
|
||||
end;
|
||||
$$;`)
|
||||
setEditorPanelTemplates([])
|
||||
openSidebar(SIDEBAR_KEYS.EDITOR_PANEL)
|
||||
} else {
|
||||
setSelectedFunction(undefined)
|
||||
setShowCreateFunctionForm(true)
|
||||
}
|
||||
}
|
||||
|
||||
const duplicateFunction = (fn: DatabaseFunction) => {
|
||||
setIsDuplicating(true)
|
||||
|
||||
const dupFn = {
|
||||
...fn,
|
||||
name: `${fn.name}_duplicate`,
|
||||
}
|
||||
|
||||
if (isInlineEditorEnabled) {
|
||||
setEditorPanelInitialPrompt('Create new database function that...')
|
||||
setEditorPanelValue(dupFn.complete_statement)
|
||||
setEditorPanelTemplates([])
|
||||
openSidebar(SIDEBAR_KEYS.EDITOR_PANEL)
|
||||
} else {
|
||||
setSelectedFunction(dupFn)
|
||||
setShowCreateFunctionForm(true)
|
||||
}
|
||||
}
|
||||
|
||||
const editFunction = (fn: DatabaseFunction) => {
|
||||
setIsDuplicating(false)
|
||||
if (isInlineEditorEnabled) {
|
||||
setEditorPanelValue(fn.complete_statement)
|
||||
setEditorPanelTemplates([])
|
||||
openSidebar(SIDEBAR_KEYS.EDITOR_PANEL)
|
||||
} else {
|
||||
setSelectedFunction(fn)
|
||||
setShowCreateFunctionForm(true)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteFunction = (fn: any) => {
|
||||
setSelectedFunction(fn)
|
||||
setShowDeleteFunctionForm(true)
|
||||
}
|
||||
|
||||
if (isPermissionsLoaded && !canReadFunctions) {
|
||||
return <NoPermission isFullPage resourceText="view database functions" />
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldSection>
|
||||
<div className="col-span-12">
|
||||
<FormHeader
|
||||
title="Database Functions"
|
||||
docsUrl={`${DOCS_URL}/guides/database/functions`}
|
||||
/>
|
||||
<FunctionsList
|
||||
createFunction={createFunction}
|
||||
duplicateFunction={duplicateFunction}
|
||||
editFunction={editFunction}
|
||||
deleteFunction={deleteFunction}
|
||||
/>
|
||||
</div>
|
||||
</ScaffoldSection>
|
||||
</ScaffoldContainer>
|
||||
<CreateFunction
|
||||
func={selectedFunction}
|
||||
visible={showCreateFunctionForm}
|
||||
onClose={() => {
|
||||
setShowCreateFunctionForm(false)
|
||||
setIsDuplicating(false)
|
||||
}}
|
||||
isDuplicating={isDuplicating}
|
||||
/>
|
||||
<DeleteFunction
|
||||
func={selectedFunction}
|
||||
visible={showDeleteFunctionForm}
|
||||
setVisible={setShowDeleteFunctionForm}
|
||||
/>
|
||||
</>
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldSection>
|
||||
<div className="col-span-12">
|
||||
<FormHeader
|
||||
title="Database Functions"
|
||||
docsUrl={`${DOCS_URL}/guides/database/functions`}
|
||||
/>
|
||||
<FunctionsList />
|
||||
</div>
|
||||
</ScaffoldSection>
|
||||
</ScaffoldContainer>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user