From df14df48f0bb89da0fb06e64ba483695cb77c634 Mon Sep 17 00:00:00 2001 From: Francesco Sansalvadore Date: Wed, 19 Nov 2025 16:28:46 +0100 Subject: [PATCH] param routing: wrappers and functions (#40574) * param routing: wrappers * param routing: functions secrets --- .../EdgeFunctionSecrets.tsx | 61 ++++++++++++------ .../Wrappers/DeleteWrapperModal.tsx | 17 ++++- .../Integrations/Wrappers/OverviewTab.tsx | 6 +- .../Integrations/Wrappers/WrapperRow.tsx | 63 +++++++++++++------ .../Integrations/Wrappers/WrapperTable.tsx | 39 +++++++++++- 5 files changed, 142 insertions(+), 44 deletions(-) diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets.tsx index 85bf4695c55..462be5e618b 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionSecrets/EdgeFunctionSecrets.tsx @@ -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() + + // Track the ID being deleted to exclude it from error checking + const deletingSecretNameRef = useRef(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 = () => { 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 = () => { )} setSelectedSecret(undefined)} + secret={selectedSecretToEdit} + visible={!!selectedSecretToEdit} + onClose={() => setSelectedSecretToEdit(null)} /> 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] }) } }} > diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/DeleteWrapperModal.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/DeleteWrapperModal.tsx index afae20cee6c..29f3c2ee67b 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/DeleteWrapperModal.tsx +++ b/apps/studio/components/interfaces/Integrations/Wrappers/DeleteWrapperModal.tsx @@ -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 } -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, diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/OverviewTab.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/OverviewTab.tsx index 04f55c70422..5601ca56c8d 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/OverviewTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Wrappers/OverviewTab.tsx @@ -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, diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx index 5f830a4acf0..ef5f4772d27 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx +++ b/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx @@ -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 } -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={} 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={} className="px-1.5" - onClick={() => setDeleteWrapperShown(true)} + onClick={() => setSelectedWrapperToDelete(wrapper.id.toString())} tooltip={{ content: { side: 'bottom', @@ -166,24 +179,34 @@ const WrapperRow = ({ wrapper }: WrapperRowProps) => { - setIsClosingEditWrapper(true)}> + { + if (!open) { + setIsClosingEditWrapper(true) + } + }} + > - { - setEditWrapperShown(false) - setIsClosingEditWrapper(false) - }} - isClosing={isClosingEditWrapper} - setIsClosing={setIsClosingEditWrapper} - /> + {selectedWrapperToEdit && ( + { + setSelectedWrapperToEdit(null) + setIsClosingEditWrapper(false) + }} + isClosing={isClosingEditWrapper} + setIsClosing={setIsClosingEditWrapper} + /> + )} - {deleteWrapperShown && ( + {selectedWrapperToDelete && ( setDeleteWrapperShown(false)} + selectedWrapper={selectedWrapperToDelete} + onClose={() => setSelectedWrapperToDelete(null)} + deletingWrapperIdRef={deletingWrapperIdRef} /> )} diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/WrapperTable.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/WrapperTable.tsx index 63bcb54f67a..5cead46c659 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/WrapperTable.tsx +++ b/apps/studio/components/interfaces/Integrations/Wrappers/WrapperTable.tsx @@ -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(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 (

@@ -65,7 +89,18 @@ export const WrapperTable = ({ isLatest = false }: WrapperTableProps) => { {(isLatest ? wrappers.slice(0, 3) : wrappers).map((x) => { - return + return ( + + ) })}