mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
param routing: wrappers and functions (#40574)
* param routing: wrappers * param routing: functions secrets
This commit is contained in:
1 parent
939f58a843
commit
df14df48f0
5 files changed
+142
-44
No files matched your search
+41
-20
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user