param routing: wrappers and functions (#40574)

* param routing: wrappers

* param routing: functions secrets
This commit is contained in:
Francesco Sansalvadore authored and GitHub committed 2025-11-19 08:28:46 -07:00
1 parent 939f58a843
commit df14df48f0
5 files changed
+142 -44

No files matched your search

@@ -1,6 +1,6 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Search } from 'lucide-react'
import { useState } from 'react'
import { useRef, useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
@@ -10,6 +10,7 @@ import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useSecretsDeleteMutation } from 'data/secrets/secrets-delete-mutation'
import { ProjectSecret, useSecretsQuery } from 'data/secrets/secrets-query'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { handleErrorOnDelete, useQueryStateWithSelect } from 'hooks/misc/useQueryStateWithSelect'
import { Badge, Card, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
import { Input } from 'ui-patterns/DataInputs/Input'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
@@ -17,15 +18,12 @@ import AddNewSecretForm from './AddNewSecretForm'
import EdgeFunctionSecret from './EdgeFunctionSecret'
import { EditSecretSheet } from './EditSecretSheet'
type SelectedProjectSecret = {
secret: ProjectSecret
op: 'delete' | 'edit'
}
const EdgeFunctionSecrets = () => {
const { ref: projectRef } = useParams()
const [searchString, setSearchString] = useState('')
const [selectedSecret, setSelectedSecret] = useState<SelectedProjectSecret>()
// Track the ID being deleted to exclude it from error checking
const deletingSecretNameRef = useRef<string | null>(null)
const { can: canReadSecrets, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
PermissionAction.SECRETS_READ,
@@ -37,10 +35,32 @@ const EdgeFunctionSecrets = () => {
projectRef: projectRef,
})
const { setValue: setSelectedSecretToEdit, value: selectedSecretToEdit } =
useQueryStateWithSelect({
urlKey: 'edit',
select: (secretName: string) =>
secretName ? data?.find((secret) => secret.name === secretName) : undefined,
enabled: !!data,
onError: () => toast.error(`Secret not found`),
})
const { setValue: setSelectedSecretToDelete, value: selectedSecretToDelete } =
useQueryStateWithSelect({
urlKey: 'delete',
select: (secretName: string) =>
secretName ? data?.find((secret) => secret.name === secretName) : undefined,
enabled: !!data,
onError: (_error, selectedId) =>
handleErrorOnDelete(deletingSecretNameRef, selectedId, `Secret not found`),
})
const { mutate: deleteSecret, isLoading: isDeleting } = useSecretsDeleteMutation({
onSuccess: () => {
toast.success(`Successfully deleted ${selectedSecret?.secret.name}`)
setSelectedSecret(undefined)
onSuccess: (_, variables) => {
toast.success(`Successfully deleted ${variables.secrets[0]}`)
setSelectedSecretToDelete(null)
},
onError: () => {
deletingSecretNameRef.current = null
},
})
@@ -105,8 +125,8 @@ const EdgeFunctionSecrets = () => {
<EdgeFunctionSecret
key={secret.name}
secret={secret}
onSelectEdit={() => setSelectedSecret({ secret, op: 'edit' })}
onSelectDelete={() => setSelectedSecret({ secret, op: 'delete' })}
onSelectEdit={() => setSelectedSecretToEdit(secret.name)}
onSelectDelete={() => setSelectedSecretToDelete(secret.name)}
/>
))
) : secrets.length === 0 && searchString.length > 0 ? (
@@ -139,22 +159,23 @@ const EdgeFunctionSecrets = () => {
)}
<EditSecretSheet
secret={selectedSecret?.secret}
visible={selectedSecret?.op === 'edit'}
onClose={() => setSelectedSecret(undefined)}
secret={selectedSecretToEdit}
visible={!!selectedSecretToEdit}
onClose={() => setSelectedSecretToEdit(null)}
/>
<ConfirmationModal
variant="destructive"
loading={isDeleting}
visible={selectedSecret?.op === 'delete'}
visible={!!selectedSecretToDelete}
confirmLabel="Delete secret"
confirmLabelLoading="Deleting secret"
title={`Confirm to delete secret "${selectedSecret?.secret.name}"`}
onCancel={() => setSelectedSecret(undefined)}
title={`Confirm to delete secret "${selectedSecretToDelete?.name}"`}
onCancel={() => setSelectedSecretToDelete(null)}
onConfirm={() => {
if (selectedSecret !== undefined) {
deleteSecret({ projectRef, secrets: [selectedSecret.secret.name] })
if (selectedSecretToDelete) {
deletingSecretNameRef.current = selectedSecretToDelete.name
deleteSecret({ projectRef, secrets: [selectedSecretToDelete.name] })
}
}}
>
@@ -1,3 +1,4 @@
import { MutableRefObject } from 'react'
import { toast } from 'sonner'
import { Modal } from 'ui'
@@ -9,15 +10,25 @@ import { getWrapperMetaForWrapper } from './Wrappers.utils'
interface DeleteWrapperModalProps {
selectedWrapper?: FDW
onClose: () => void
deletingWrapperIdRef?: MutableRefObject<string | null>
}
const DeleteWrapperModal = ({ selectedWrapper, onClose }: DeleteWrapperModalProps) => {
const DeleteWrapperModal = ({
selectedWrapper,
onClose,
deletingWrapperIdRef,
}: DeleteWrapperModalProps) => {
const { data: project } = useSelectedProjectQuery()
const { mutate: deleteFDW, isLoading: isDeleting } = useFDWDeleteMutation({
onSuccess: () => {
toast.success(`Successfully disabled ${selectedWrapper?.name} foreign data wrapper`)
onClose()
},
onError: () => {
if (deletingWrapperIdRef) {
deletingWrapperIdRef.current = null
}
},
})
const wrapperMeta = getWrapperMetaForWrapper(selectedWrapper)
@@ -26,6 +37,10 @@ const DeleteWrapperModal = ({ selectedWrapper, onClose }: DeleteWrapperModalProp
if (!selectedWrapper) return console.error('Wrapper is required')
if (!wrapperMeta) return console.error('Wrapper meta is required')
if (deletingWrapperIdRef) {
deletingWrapperIdRef.current = selectedWrapper.id.toString()
}
deleteFDW({
projectRef: project?.ref,
connectionString: project?.connectionString,
@@ -1,6 +1,7 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import Link from 'next/link'
import { useState } from 'react'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
@@ -28,7 +29,10 @@ import { WrapperTable } from './WrapperTable'
export const WrapperOverviewTab = () => {
const { id } = useParams()
const { data: project } = useSelectedProjectQuery()
const [createWrapperShown, setCreateWrapperShown] = useState(false)
const [createWrapperShown, setCreateWrapperShown] = useQueryState(
'new',
parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
)
const { can: canCreateWrapper } = useAsyncCheckPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
@@ -2,7 +2,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { partition } from 'lodash'
import { ChevronRight, Edit, ExternalLink, Table2, Trash } from 'lucide-react'
import Link from 'next/link'
import { useState } from 'react'
import { MutableRefObject, useState } from 'react'
import { useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
@@ -25,18 +25,31 @@ import { formatWrapperTables } from './Wrappers.utils'
interface WrapperRowProps {
wrapper: FDW
wrappers: FDW[]
selectedWrapperToEdit?: FDW
selectedWrapperToDelete?: FDW
setSelectedWrapperToEdit: (value: string | null) => void
setSelectedWrapperToDelete: (value: string | null) => void
deletingWrapperIdRef: MutableRefObject<string | null>
}
const WrapperRow = ({ wrapper }: WrapperRowProps) => {
const WrapperRow = ({
wrapper,
wrappers,
selectedWrapperToEdit,
selectedWrapperToDelete,
setSelectedWrapperToEdit,
setSelectedWrapperToDelete,
deletingWrapperIdRef,
}: WrapperRowProps) => {
const { ref, id } = useParams()
const { can: canManageWrappers } = useAsyncCheckPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'wrappers'
)
const [editWrapperShown, setEditWrapperShown] = useState(false)
const editWrapperShown = selectedWrapperToEdit?.id === wrapper.id
const [isClosingEditWrapper, setIsClosingEditWrapper] = useState(false)
const [deleteWrapperShown, setDeleteWrapperShown] = useState(false)
const integration = INTEGRATIONS.find((i) => i.id === id)
@@ -138,7 +151,7 @@ const WrapperRow = ({ wrapper }: WrapperRowProps) => {
type="default"
icon={<Edit strokeWidth={1.5} />}
className="px-1.5"
onClick={() => setEditWrapperShown(true)}
onClick={() => setSelectedWrapperToEdit(wrapper.id.toString())}
tooltip={{
content: {
side: 'bottom',
@@ -153,7 +166,7 @@ const WrapperRow = ({ wrapper }: WrapperRowProps) => {
disabled={!canManageWrappers}
icon={<Trash strokeWidth={1.5} />}
className="px-1.5"
onClick={() => setDeleteWrapperShown(true)}
onClick={() => setSelectedWrapperToDelete(wrapper.id.toString())}
tooltip={{
content: {
side: 'bottom',
@@ -166,24 +179,34 @@ const WrapperRow = ({ wrapper }: WrapperRowProps) => {
</div>
</TableCell>
</TableRow>
<Sheet open={editWrapperShown} onOpenChange={() => setIsClosingEditWrapper(true)}>
<Sheet
open={editWrapperShown}
onOpenChange={(open) => {
if (!open) {
setIsClosingEditWrapper(true)
}
}}
>
<SheetContent size="lg" tabIndex={undefined}>
<EditWrapperSheet
wrapper={wrapper}
wrapperMeta={integration.meta}
onClose={() => {
setEditWrapperShown(false)
setIsClosingEditWrapper(false)
}}
isClosing={isClosingEditWrapper}
setIsClosing={setIsClosingEditWrapper}
/>
{selectedWrapperToEdit && (
<EditWrapperSheet
wrapper={selectedWrapperToEdit}
wrapperMeta={integration.meta}
onClose={() => {
setSelectedWrapperToEdit(null)
setIsClosingEditWrapper(false)
}}
isClosing={isClosingEditWrapper}
setIsClosing={setIsClosingEditWrapper}
/>
)}
</SheetContent>
</Sheet>
{deleteWrapperShown && (
{selectedWrapperToDelete && (
<DeleteWrapperModal
selectedWrapper={wrapper}
onClose={() => setDeleteWrapperShown(false)}
selectedWrapper={selectedWrapperToDelete}
onClose={() => setSelectedWrapperToDelete(null)}
deletingWrapperIdRef={deletingWrapperIdRef}
/>
)}
</>
@@ -1,8 +1,10 @@
import { useMemo } from 'react'
import { useMemo, useRef } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import { useFDWsQuery } from 'data/fdw/fdws-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { handleErrorOnDelete, useQueryStateWithSelect } from 'hooks/misc/useQueryStateWithSelect'
import {
Card,
CardContent,
@@ -39,6 +41,28 @@ export const WrapperTable = ({ isLatest = false }: WrapperTableProps) => {
[data, integration]
)
// Track the ID being deleted to exclude it from error checking
const deletingWrapperIdRef = useRef<string | null>(null)
const { setValue: setSelectedWrapperToEdit, value: selectedWrapperToEdit } =
useQueryStateWithSelect({
urlKey: 'edit',
select: (wrapperId: string) =>
wrapperId ? wrappers.find((w) => w.id.toString() === wrapperId) : undefined,
enabled: !!wrappers.length,
onError: () => toast.error(`Wrapper not found`),
})
const { setValue: setSelectedWrapperToDelete, value: selectedWrapperToDelete } =
useQueryStateWithSelect({
urlKey: 'delete',
select: (wrapperId: string) =>
wrapperId ? wrappers.find((w) => w.id.toString() === wrapperId) : undefined,
enabled: !!wrappers.length,
onError: (_error, selectedId) =>
handleErrorOnDelete(deletingWrapperIdRef, selectedId, `Wrapper not found`),
})
if (!integration || integration.type !== 'wrapper') {
return (
<p className="text-foreground-light text-sm">
@@ -65,7 +89,18 @@ export const WrapperTable = ({ isLatest = false }: WrapperTableProps) => {
</TableHeader>
<TableBody className="[&_td]:py-0 [&_tr]:h-[50px] [&_tr]:border-dotted bg-surface-100">
{(isLatest ? wrappers.slice(0, 3) : wrappers).map((x) => {
return <WrapperRow key={x.id} wrapper={x} />
return (
<WrapperRow
key={x.id}
wrapper={x}
wrappers={wrappers}
selectedWrapperToEdit={selectedWrapperToEdit}
selectedWrapperToDelete={selectedWrapperToDelete}
setSelectedWrapperToEdit={setSelectedWrapperToEdit}
setSelectedWrapperToDelete={setSelectedWrapperToDelete}
deletingWrapperIdRef={deletingWrapperIdRef}
/>
)
})}
</TableBody>
</Table>