From e48c909b317b78e2e23d8c10bb6fb10ea026b3a7 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 13 Feb 2026 16:18:28 +0800 Subject: [PATCH] Chore/deprecate use query state with select part 04 (#42747) ## Context Related to FE-2461 More refactoring to clean up usage of `useQueryStateWithSelect`, in the following pages - Auth OAuth Apps - Cron Jobs - Vaults Secrets Management - Database Hooks - Wrappers ## To test In each of those pages, verify that - [ ] Clicking the "new" cta updates the URL params, and refreshing should re-open the sheet - [ ] Editing an existing item should update URL params, and refreshing should re-open the sheet with the right item - Verify that if the URL param has the wrong id, page should not open the sheet, show a toast, and reset the URL param - [ ] Deleting an existing item should update URL params, and refreshing should re-open the sheet with the right item - Verify that if the URL param has the wrong id, page should not open the sheet, show a toast, and reset the URL param ## Summary by CodeRabbit * **Bug Fixes** * Added "item not found" toasts and clearer feedback after create/update/delete actions; prevent opening create flows when server features are disabled. * **Style** * Standardized button labels, sizes and modal/dialog spacing; refined table column widths and inline code formatting for readability. * **Refactor** * Simplified UI state flows across OAuth Apps, Hooks/Webhooks, Cron Jobs, Vault Secrets, and Wrappers to use consistent URL-driven interactions and centralized modals. --- .../OAuthApps/CreateOrUpdateOAuthAppSheet.tsx | 25 +- .../Auth/OAuthApps/DeleteOAuthAppModal.tsx | 4 +- .../Auth/OAuthApps/OAuthAppsList.tsx | 139 ++++----- .../Database/Hooks/DeleteHookModal.tsx | 75 ++--- .../Database/Hooks/EditHookPanel.tsx | 40 ++- .../Database/Hooks/FormContents.tsx | 4 +- .../Database/Hooks/HooksList/HookList.tsx | 31 +- .../Database/Hooks/HooksList/HooksList.tsx | 42 +-- .../Database/Hooks/HooksList/SchemaTable.tsx | 22 +- .../CronJobs/CronJobTableCell.tsx | 11 +- .../Integrations/CronJobs/CronJobsTab.tsx | 61 ++-- .../CronJobs/CronJobsTab.useCronJobsData.ts | 4 + .../Integrations/CronJobs/DeleteCronJob.tsx | 52 ++-- .../Vault/Secrets/AddNewSecretModal.tsx | 40 ++- .../Vault/Secrets/DeleteSecretModal.tsx | 50 ++-- .../Vault/Secrets/EditSecretModal.tsx | 59 ++-- .../Integrations/Vault/Secrets/SecretRow.tsx | 26 +- .../Vault/Secrets/Secrets.utils.tsx | 19 +- .../Vault/Secrets/SecretsManagement.tsx | 97 ++----- .../__tests__/EditSecretModal.test.tsx | 70 ++--- .../Integrations/Webhooks/ListTab.tsx | 80 +---- .../Integrations/Webhooks/OverviewTab.tsx | 6 +- .../Wrappers/DeleteWrapperModal.tsx | 82 ++++-- .../Integrations/Wrappers/WrapperRow.tsx | 273 +++++++----------- .../Integrations/Wrappers/WrapperTable.tsx | 148 +++++----- .../Integrations/Wrappers/WrappersTab.tsx | 14 +- apps/studio/data/vault/keys.ts | 4 +- .../vault-secret-decrypted-value-query.ts | 5 +- 28 files changed, 681 insertions(+), 802 deletions(-) diff --git a/apps/studio/components/interfaces/Auth/OAuthApps/CreateOrUpdateOAuthAppSheet.tsx b/apps/studio/components/interfaces/Auth/OAuthApps/CreateOrUpdateOAuthAppSheet.tsx index 5165083cc7f44..3c690d92f9731 100644 --- a/apps/studio/components/interfaces/Auth/OAuthApps/CreateOrUpdateOAuthAppSheet.tsx +++ b/apps/studio/components/interfaces/Auth/OAuthApps/CreateOrUpdateOAuthAppSheet.tsx @@ -4,12 +4,6 @@ import type { OAuthClient, UpdateOAuthClientParams, } from '@supabase/supabase-js' -import { Plus, Trash2, Upload, X } from 'lucide-react' -import { type ChangeEvent, useEffect, useRef, useState } from 'react' -import { useFieldArray, useForm } from 'react-hook-form' -import { toast } from 'sonner' -import * as z from 'zod' - import { useParams } from 'common' import { InlineLink } from 'components/ui/InlineLink' import Panel from 'components/ui/Panel' @@ -18,15 +12,20 @@ import { useOAuthServerAppCreateMutation } from 'data/oauth-server-apps/oauth-se import { useOAuthServerAppRegenerateSecretMutation } from 'data/oauth-server-apps/oauth-server-app-regenerate-secret-mutation' import { useOAuthServerAppUpdateMutation } from 'data/oauth-server-apps/oauth-server-app-update-mutation' import { DOCS_URL } from 'lib/constants' +import { Plus, Trash2, Upload, X } from 'lucide-react' +import { useEffect, useRef, useState, type ChangeEvent } from 'react' +import { useFieldArray, useForm } from 'react-hook-form' +import { toast } from 'sonner' import { Button, + cn, + Form_Shadcn_, FormControl_Shadcn_, FormDescription_Shadcn_, FormField_Shadcn_, FormItem_Shadcn_, FormLabel_Shadcn_, FormMessage_Shadcn_, - Form_Shadcn_, Input_Shadcn_, Separator, Sheet, @@ -37,11 +36,11 @@ import { SheetSection, SheetTitle, Switch, - cn, } from 'ui' import { Input } from 'ui-patterns/DataInputs/Input' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' +import * as z from 'zod' interface CreateOrUpdateOAuthAppSheetProps { visible: boolean @@ -87,12 +86,14 @@ export const CreateOrUpdateOAuthAppSheet = ({ onCancel, }: CreateOrUpdateOAuthAppSheetProps) => { const { ref: projectRef } = useParams() - const isEditMode = !!appToEdit - const [showRegenerateDialog, setShowRegenerateDialog] = useState(false) const uploadButtonRef = useRef(null) + + const [showRegenerateDialog, setShowRegenerateDialog] = useState(false) const [logoFile, setLogoFile] = useState() const [logoUrl, setLogoUrl] = useState() const [logoRemoved, setLogoRemoved] = useState(false) + + const isEditMode = !!appToEdit const hasLogo = logoUrl !== undefined const isPublicClient = appToEdit?.client_type === 'public' @@ -391,10 +392,10 @@ export const CreateOrUpdateOAuthAppSheet = ({ )} diff --git a/apps/studio/components/interfaces/Auth/OAuthApps/DeleteOAuthAppModal.tsx b/apps/studio/components/interfaces/Auth/OAuthApps/DeleteOAuthAppModal.tsx index 5b228d8ed8bd8..df996225bd968 100644 --- a/apps/studio/components/interfaces/Auth/OAuthApps/DeleteOAuthAppModal.tsx +++ b/apps/studio/components/interfaces/Auth/OAuthApps/DeleteOAuthAppModal.tsx @@ -1,6 +1,5 @@ import type { OAuthClient } from '@supabase/supabase-js' import { useParams } from 'common' - import { useProjectEndpointQuery } from 'data/config/project-endpoint-query' import type { OAuthServerAppDeleteVariables } from 'data/oauth-server-apps/oauth-server-app-delete-mutation' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' @@ -39,7 +38,8 @@ export const DeleteOAuthAppModal = ({ visible={visible} title={ <> - Confirm to delete OAuth app {selectedApp?.client_name} + Confirm to delete OAuth app{' '} + {selectedApp?.client_name} } confirmLabel="Confirm delete" diff --git a/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx b/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx index 2d31a8dcbe77c..817103165eb03 100644 --- a/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx +++ b/apps/studio/components/interfaces/Auth/OAuthApps/OAuthAppsList.tsx @@ -1,10 +1,4 @@ import type { OAuthClient } from '@supabase/supabase-js' -import { Edit, MoreVertical, Plus, RotateCw, Search, Trash, X } from 'lucide-react' -import Link from 'next/link' -import { parseAsBoolean, parseAsStringLiteral, useQueryState } from 'nuqs' -import { useEffect, useMemo, useRef, useState } from 'react' -import { toast } from 'sonner' - import { useParams } from 'common' import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' @@ -14,14 +8,18 @@ import { useProjectEndpointQuery } from 'data/config/project-endpoint-query' import { useOAuthServerAppDeleteMutation } from 'data/oauth-server-apps/oauth-server-app-delete-mutation' import { useOAuthServerAppRegenerateSecretMutation } from 'data/oauth-server-apps/oauth-server-app-regenerate-secret-mutation' import { useOAuthServerAppsQuery } from 'data/oauth-server-apps/oauth-server-apps-query' -import { handleErrorOnDelete, useQueryStateWithSelect } from 'hooks/misc/useQueryStateWithSelect' +import { Edit, MoreVertical, Plus, RotateCw, Search, Trash, X } from 'lucide-react' +import Link from 'next/link' +import { parseAsBoolean, parseAsString, parseAsStringLiteral, useQueryState } from 'nuqs' +import { useEffect, useMemo, useState } from 'react' +import { toast } from 'sonner' import { - Badge, Button, Card, DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, Input, Table, @@ -36,6 +34,7 @@ import { Admonition } from 'ui-patterns/admonition' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { TimestampInfo } from 'ui-patterns/TimestampInfo' + import { CreateOrUpdateOAuthAppSheet } from './CreateOrUpdateOAuthAppSheet' import { DeleteOAuthAppModal } from './DeleteOAuthAppModal' import { NewOAuthAppBanner } from './NewOAuthAppBanner' @@ -62,77 +61,64 @@ type OAuthAppsSortOrder = OAuthAppsSort extends `${string}:${infer Order}` ? Ord export const OAuthAppsList = () => { const { ref: projectRef } = useParams() - const { data: authConfig, isPending: isAuthConfigLoading } = useAuthConfigQuery({ projectRef }) + const { + data: authConfig, + isPending: isAuthConfigLoading, + isSuccess: isSuccessAuthConfig, + } = useAuthConfigQuery({ projectRef }) const isOAuthServerEnabled = !!authConfig?.OAUTH_SERVER_ENABLED + const [newOAuthApp, setNewOAuthApp] = useState(undefined) const [showRegenerateDialog, setShowRegenerateDialog] = useState(false) const [selectedApp, setSelectedApp] = useState() const [filteredRegistrationTypes, setFilteredRegistrationTypes] = useState([]) const [filteredClientTypes, setFilteredClientTypes] = useState([]) - const deletingOAuthAppIdRef = useRef(null) + const [filterString, setFilterString] = useState('') - const { data, isPending: isLoading, isError, error } = useOAuthServerAppsQuery({ projectRef }) + const { data: endpointData } = useProjectEndpointQuery({ projectRef }) + const { + data, + error, + isPending: isLoading, + isSuccess, + isError, + } = useOAuthServerAppsQuery({ projectRef }) + const oAuthApps = useMemo(() => data?.clients || [], [data]) const { mutateAsync: regenerateSecret, isPending: isRegenerating } = useOAuthServerAppRegenerateSecretMutation({ onSuccess: (data) => { - if (data) { - setNewOAuthApp(data) - } + if (data) setNewOAuthApp(data) }, }) - const { data: endpointData } = useProjectEndpointQuery({ projectRef }) - - const oAuthApps = useMemo(() => data?.clients || [], [data]) + const [sort, setSort] = useQueryState( + 'sort', + parseAsStringLiteral(OAUTH_APPS_SORT_VALUES).withDefault('name:asc') + ) const [showCreateSheet, setShowCreateSheet] = useQueryState( 'new', - parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true }) + parseAsBoolean.withDefault(false) ) - // Prevent opening the create sheet if OAuth Server is disabled - useEffect(() => { - if (!isOAuthServerEnabled && showCreateSheet) { - setShowCreateSheet(false) - } - }, [isOAuthServerEnabled, showCreateSheet, setShowCreateSheet]) + const [selectedAppToEdit, setSelectedAppToEdit] = useQueryState('edit', parseAsString) + const appToEdit = oAuthApps?.find((app) => app.client_id === selectedAppToEdit) - const { setValue: setSelectedAppToEdit, value: appToEdit } = useQueryStateWithSelect({ - urlKey: 'edit', - select: (client_id: string) => - client_id ? oAuthApps?.find((app) => app.client_id === client_id) : undefined, - enabled: !!oAuthApps?.length, - onError: (_error, selectedId) => - handleErrorOnDelete(deletingOAuthAppIdRef, selectedId, `OAuth App not found`), - }) + const [selectedAppToDelete, setSelectedAppToDelete] = useQueryState('delete', parseAsString) + const appToDelete = oAuthApps?.find((app) => app.client_id === selectedAppToDelete) - const { setValue: setSelectedAppToDelete, value: appToDelete } = useQueryStateWithSelect({ - urlKey: 'delete', - select: (client_id: string) => - client_id ? oAuthApps?.find((app) => app.client_id === client_id) : undefined, - enabled: !!oAuthApps?.length, - onError: (_error, selectedId) => - handleErrorOnDelete(deletingOAuthAppIdRef, selectedId, `OAuth App not found`), - }) - - const { mutate: deleteOAuthApp, isPending: isDeletingApp } = useOAuthServerAppDeleteMutation({ + const { + mutate: deleteOAuthApp, + isPending: isDeletingApp, + isSuccess: isSuccessDelete, + } = useOAuthServerAppDeleteMutation({ onSuccess: () => { toast.success(`Successfully deleted OAuth app`) setSelectedAppToDelete(null) }, - onError: () => { - deletingOAuthAppIdRef.current = null - }, }) - const [filterString, setFilterString] = useState('') - - const [sort, setSort] = useQueryState( - 'sort', - parseAsStringLiteral(OAUTH_APPS_SORT_VALUES).withDefault('name:asc') - ) - const filteredAndSortedOAuthApps = useMemo(() => { const filtered = filterOAuthApps({ apps: oAuthApps, @@ -194,6 +180,27 @@ export const OAuthAppsList = () => { const isEditMode = !!appToEdit const isCreateOrUpdateSheetVisible = isCreateMode || isEditMode + // Prevent opening the create sheet if OAuth Server is disabled + useEffect(() => { + if (isSuccessAuthConfig && !isOAuthServerEnabled && showCreateSheet) { + setShowCreateSheet(false) + } + }, [isSuccessAuthConfig, isOAuthServerEnabled, showCreateSheet, setShowCreateSheet]) + + useEffect(() => { + if (isSuccess && !!selectedAppToEdit && !appToEdit) { + toast('App not found') + setSelectedAppToEdit(null) + } + }, [appToEdit, isSuccess, selectedAppToEdit, setSelectedAppToEdit]) + + useEffect(() => { + if (isSuccess && !!selectedAppToDelete && !appToDelete && !isSuccessDelete) { + toast('App not found') + setSelectedAppToDelete(null) + } + }, [appToDelete, isSuccess, isSuccessDelete, selectedAppToDelete, setSelectedAppToDelete]) + if (isAuthConfigLoading || (isOAuthServerEnabled && isLoading)) { return } @@ -291,7 +298,7 @@ export const OAuthAppsList = () => { - + Name @@ -340,7 +347,7 @@ export const OAuthAppsList = () => { {filteredAndSortedOAuthApps.length > 0 && filteredAndSortedOAuthApps.map((app) => ( - + - {app.client_id} - - - {app.client_type} - - - {app.registration_type} + {app.client_id} - - + {app.client_type} + {app.registration_type} + +
@@ -376,7 +383,7 @@ export const OAuthAppsList = () => { }} > -

Update

+

Edit OAuth app

{app.client_type === 'confidential' && ( {

Regenerate client secret

)} + setSelectedAppToDelete(app.client_id)} > -

Delete

+

Delete OAuth app

@@ -434,7 +442,6 @@ export const OAuthAppsList = () => { selectedApp={appToDelete} setVisible={setSelectedAppToDelete} onDelete={(params: Parameters[0]) => { - deletingOAuthAppIdRef.current = params.clientId ?? null deleteOAuthApp(params) }} isLoading={isDeletingApp} diff --git a/apps/studio/components/interfaces/Database/Hooks/DeleteHookModal.tsx b/apps/studio/components/interfaces/Database/Hooks/DeleteHookModal.tsx index 009a7ab82c814..8c8b0832a1063 100644 --- a/apps/studio/components/interfaces/Database/Hooks/DeleteHookModal.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/DeleteHookModal.tsx @@ -1,44 +1,42 @@ -import type { PostgresTrigger } from '@supabase/postgres-meta' -import { toast } from 'sonner' - import { TextConfirmModal } from 'components/ui/TextConfirmModalWrapper' import { useDatabaseTriggerDeleteMutation } from 'data/database-triggers/database-trigger-delete-mutation' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { parseAsString, useQueryState } from 'nuqs' +import { useEffect } from 'react' +import { toast } from 'sonner' -interface DeleteHookModalProps { - visible: boolean - selectedHook?: PostgresTrigger - onClose: () => void - onDeleteStart?: (hookId: string) => void -} - -const DeleteHookModal = ({ - selectedHook, - visible, - onClose, - onDeleteStart, -}: DeleteHookModalProps) => { - const { name, schema } = selectedHook ?? {} +import { useDatabaseHooksQuery } from '@/data/database-triggers/database-triggers-query' +export const DeleteHookModal = () => { const { data: project } = useSelectedProjectQuery() - const { mutate: deleteDatabaseTrigger, isPending: isDeleting } = useDatabaseTriggerDeleteMutation( - { - onSuccess: () => { - toast.success(`Successfully deleted ${name}`) - onClose() - }, - } + + const { data: hooks = [], isSuccess } = useDatabaseHooksQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + const [selectedHookIdToDelete, setSelectedHookIdToDelete] = useQueryState( + 'delete', + parseAsString.withDefault('') ) + const selectedHook = hooks.find((hook) => hook.id.toString() === selectedHookIdToDelete) + const { name, schema } = selectedHook ?? {} + + const { + mutate: deleteDatabaseTrigger, + isPending: isDeleting, + isSuccess: isSuccessDelete, + } = useDatabaseTriggerDeleteMutation({ + onSuccess: () => { + toast.success(`Successfully deleted ${name}`) + setSelectedHookIdToDelete(null) + }, + }) async function handleDelete() { - if (!project) { - return console.error('Project ref is required') - } - if (!selectedHook) { - return toast.error('Unable find selected hook') - } + if (!project) return console.error('Project ref is required') + if (!selectedHook) return toast.error('Unable to find selected hook') - onDeleteStart?.(selectedHook.id.toString()) deleteDatabaseTrigger({ trigger: selectedHook, projectRef: project.ref, @@ -46,12 +44,19 @@ const DeleteHookModal = ({ }) } + useEffect(() => { + if (isSuccess && !!selectedHookIdToDelete && !selectedHook && !isSuccessDelete) { + toast('Webhook not found') + setSelectedHookIdToDelete(null) + } + }, [isSuccess, isSuccessDelete, selectedHook, selectedHookIdToDelete, setSelectedHookIdToDelete]) + return ( onClose()} + visible={!!selectedHook} + size="small" + onCancel={() => setSelectedHookIdToDelete(null)} onConfirm={handleDelete} title="Delete database webhook" loading={isDeleting} @@ -68,5 +73,3 @@ const DeleteHookModal = ({ /> ) } - -export default DeleteHookModal diff --git a/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx b/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx index f4f43edf07815..1222c74c94fcc 100644 --- a/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/EditHookPanel.tsx @@ -3,6 +3,7 @@ import { PGTriggerCreate } from '@supabase/pg-meta/src/pg-meta-triggers' import type { PostgresTrigger } from '@supabase/postgres-meta' import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'common' +import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs' import { useEffect, useState } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import { toast } from 'sonner' @@ -13,17 +14,12 @@ import { FormSchema, WebhookFormValues } from './EditHookPanel.constants' import { FormContents } from './FormContents' import { useDatabaseTriggerCreateMutation } from '@/data/database-triggers/database-trigger-create-mutation' import { useDatabaseTriggerUpdateMutation } from '@/data/database-triggers/database-trigger-update-transaction-mutation' +import { useDatabaseHooksQuery } from '@/data/database-triggers/database-triggers-query' import { tableEditorQueryOptions } from '@/data/table-editor/table-editor-query' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useConfirmOnClose, type ConfirmOnCloseModalProps } from '@/hooks/ui/useConfirmOnClose' import { uuidv4 } from '@/lib/helpers' -export interface EditHookPanelProps { - visible: boolean - selectedHook?: PostgresTrigger - onClose: () => void -} - export type HTTPArgument = { id: string; name: string; value: string } export const isEdgeFunction = ({ @@ -80,11 +76,29 @@ const parseParameters = (selectedHook?: PostgresTrigger): HTTPArgument[] => { })) } -export const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelProps) => { +export const EditHookPanel = () => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() const [isLoadingTable, setIsLoadingTable] = useState(false) + const { data: hooks = [], isSuccess } = useDatabaseHooksQuery({ + projectRef: project?.ref, + connectionString: project?.connectionString, + }) + + const [showCreateHookForm, setShowCreateHookForm] = useQueryState( + 'new', + parseAsBoolean.withDefault(false) + ) + + const [selectedHookIdToEdit, setSelectedHookIdToEdit] = useQueryState( + 'edit', + parseAsString.withDefault('') + ) + const selectedHook = hooks.find((hook) => hook.id.toString() === selectedHookIdToEdit) + + const visible = showCreateHookForm || !!selectedHook + const { mutate: createDatabaseTrigger, isPending: isCreating } = useDatabaseTriggerCreateMutation( { onSuccess: (res) => { @@ -135,6 +149,18 @@ export const EditHookPanel = ({ visible, selectedHook, onClose }: EditHookPanelP }, }) + const onClose = () => { + setShowCreateHookForm(false) + setSelectedHookIdToEdit(null) + } + + useEffect(() => { + if (isSuccess && !!selectedHookIdToEdit && !selectedHook) { + toast('Webhook not found') + setSelectedHookIdToEdit(null) + } + }, [isSuccess, selectedHook, selectedHookIdToEdit, setSelectedHookIdToEdit]) + // Reset form when panel opens with new selectedHook useEffect(() => { if (visible) { diff --git a/apps/studio/components/interfaces/Database/Hooks/FormContents.tsx b/apps/studio/components/interfaces/Database/Hooks/FormContents.tsx index a74690c7d35f6..f7f95fb71a387 100644 --- a/apps/studio/components/interfaces/Database/Hooks/FormContents.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/FormContents.tsx @@ -118,7 +118,9 @@ export const FormContents = ({ form, selectedHook }: FormContentsProps) => { -

Do not use spaces/whitespaces

+

+ Do not use spaces/whitespaces +

)} /> diff --git a/apps/studio/components/interfaces/Database/Hooks/HooksList/HookList.tsx b/apps/studio/components/interfaces/Database/Hooks/HooksList/HookList.tsx index 4b691f27578d8..2b261fb2da989 100644 --- a/apps/studio/components/interfaces/Database/Hooks/HooksList/HookList.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/HooksList/HookList.tsx @@ -1,9 +1,4 @@ -import { PostgresTrigger } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { includes, noop } from 'lodash' -import { Edit3, MoreVertical, Trash } from 'lucide-react' -import Image from 'next/legacy/image' - import { useParams } from 'common' import Table from 'components/to-be-cleaned/Table' import { ButtonTooltip } from 'components/ui/ButtonTooltip' @@ -11,6 +6,10 @@ import { useDatabaseHooksQuery } from 'data/database-triggers/database-triggers- import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { BASE_PATH } from 'lib/constants' +import { includes } from 'lodash' +import { Edit3, MoreVertical, Trash } from 'lucide-react' +import Image from 'next/legacy/image' +import { parseAsString, useQueryState } from 'nuqs' import { Badge, Button, @@ -24,16 +23,9 @@ import { export interface HookListProps { schema: string filterString: string - editHook: (hook: PostgresTrigger) => void - deleteHook: (hook: PostgresTrigger) => void } -export const HookList = ({ - schema, - filterString, - editHook = noop, - deleteHook = noop, -}: HookListProps) => { +export const HookList = ({ schema, filterString }: HookListProps) => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() const { data: hooks } = useDatabaseHooksQuery({ @@ -41,6 +33,9 @@ export const HookList = ({ connectionString: project?.connectionString, }) + const [, setSelectedHookIdToEdit] = useQueryState('edit', parseAsString.withDefault('')) + const [, setSelectedHookIdToDelete] = useQueryState('delete', parseAsString.withDefault('')) + const restUrl = project?.restUrl const restUrlTld = restUrl ? new URL(restUrl).hostname.split('.').pop() : 'co' @@ -109,12 +104,18 @@ export const HookList = ({ <> - editHook(x)}> + setSelectedHookIdToEdit(x.id.toString())} + >

Edit hook

- deleteHook(x)}> + setSelectedHookIdToDelete(x.id.toString())} + >

Delete hook

diff --git a/apps/studio/components/interfaces/Database/Hooks/HooksList/HooksList.tsx b/apps/studio/components/interfaces/Database/Hooks/HooksList/HooksList.tsx index e87905d435aec..5c230a8a26ad5 100644 --- a/apps/studio/components/interfaces/Database/Hooks/HooksList/HooksList.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/HooksList/HooksList.tsx @@ -1,9 +1,4 @@ -import { PostgresTrigger } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { includes, map as lodashMap, uniqBy } from 'lodash' -import { Search } from 'lucide-react' -import { useState } from 'react' - import AlertError from 'components/ui/AlertError' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { DocsButton } from 'components/ui/DocsButton' @@ -12,26 +7,23 @@ import { useDatabaseHooksQuery } from 'data/database-triggers/database-triggers- import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { DOCS_URL } from 'lib/constants' -import { noop } from 'lib/void' +import { includes, map as lodashMap, uniqBy } from 'lodash' +import { Search } from 'lucide-react' +import { parseAsBoolean, useQueryState } from 'nuqs' +import { useState } from 'react' import { Input } from 'ui' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' + import { HooksListEmpty } from './HooksListEmpty' import { SchemaTable } from './SchemaTable' -export interface HooksListProps { - createHook: () => void - editHook: (hook: PostgresTrigger) => void - deleteHook: (hook: PostgresTrigger) => void -} - -export const HooksList = ({ - createHook = noop, - editHook = noop, - deleteHook = noop, -}: HooksListProps) => { +export const HooksList = () => { const { data: project } = useSelectedProjectQuery() + + const [, setShowCreateHookForm] = useQueryState('new', parseAsBoolean.withDefault(false)) + const { - data: hooks, + data: hooks = [], isPending: isLoading, isSuccess, isError, @@ -42,7 +34,7 @@ export const HooksList = ({ }) const [filterString, setFilterString] = useState('') - const filteredHooks = (hooks || []).filter((x: any) => + const filteredHooks = hooks.filter((x) => includes(x.name.toLowerCase(), filterString.toLowerCase()) ) const filteredHookSchemas = lodashMap(uniqBy(filteredHooks, 'schema'), 'schema') @@ -66,7 +58,7 @@ export const HooksList = ({
createHook()} + onClick={() => setShowCreateHookForm(true)} disabled={!isPermissionsLoaded || !canCreateWebhooks} tooltip={{ content: { @@ -102,14 +94,8 @@ export const HooksList = ({ onResetFilter={() => setFilterString('')} /> )} - {filteredHookSchemas.map((schema: any) => ( - + {filteredHookSchemas.map((schema) => ( + ))} ))} diff --git a/apps/studio/components/interfaces/Database/Hooks/HooksList/SchemaTable.tsx b/apps/studio/components/interfaces/Database/Hooks/HooksList/SchemaTable.tsx index 0ebbc7dcd7650..871321ef27d3e 100644 --- a/apps/studio/components/interfaces/Database/Hooks/HooksList/SchemaTable.tsx +++ b/apps/studio/components/interfaces/Database/Hooks/HooksList/SchemaTable.tsx @@ -1,22 +1,13 @@ -import { PostgresTrigger } from '@supabase/postgres-meta' -import { noop } from 'lodash' - import Table from 'components/to-be-cleaned/Table' + import { HookList } from './HookList' interface SchemaTableProps { schema: string filterString: string - editHook: (hook: PostgresTrigger) => void - deleteHook: (hook: PostgresTrigger) => void } -export const SchemaTable = ({ - schema, - filterString, - editHook = noop, - deleteHook = noop, -}: SchemaTableProps) => { +export const SchemaTable = ({ schema, filterString }: SchemaTableProps) => { return (
@@ -44,14 +35,7 @@ export const SchemaTable = ({ } - body={ - - } + body={} />
) diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx index 77b488e4a196a..be8d0c1e921b4 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx @@ -1,14 +1,13 @@ import parser from 'cron-parser' +import { useDatabaseCronJobRunCommandMutation } from 'data/database-cron-jobs/database-cron-job-run-mutation' +import { CronJob } from 'data/database-cron-jobs/database-cron-jobs-infinite-query' +import { useDatabaseCronJobToggleMutation } from 'data/database-cron-jobs/database-cron-jobs-toggle-mutation' import dayjs from 'dayjs' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Copy, Edit, Minus, MoreVertical, Play, Trash } from 'lucide-react' import { parseAsString, useQueryState } from 'nuqs' import { useState } from 'react' import { toast } from 'sonner' - -import { useDatabaseCronJobRunCommandMutation } from 'data/database-cron-jobs/database-cron-job-run-mutation' -import { CronJob } from 'data/database-cron-jobs/database-cron-jobs-infinite-query' -import { useDatabaseCronJobToggleMutation } from 'data/database-cron-jobs/database-cron-jobs-toggle-mutation' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Badge, Button, @@ -150,7 +149,7 @@ export const CronJobTableCell = ({ onClick={(e) => e.stopPropagation()} /> - + { const { data: org } = useSelectedOrganizationQuery() const [searchQuery, setSearchQuery] = useQueryState('search', parseAsString.withDefault('')) + + const [isDirty, setIsDirty] = useState(false) const [search, setSearch] = useState(searchQuery) const handleSearchSubmit = () => { @@ -51,29 +52,15 @@ export const CronjobsTab = () => { searchQuery, }) - const deletingCronJobIdRef = useRef(null) + const [createCronJobSheetShown, setCreateCronJobSheetShown] = useQueryState( + 'new', + parseAsBoolean.withDefault(false).withOptions({ clearOnDefault: true }) + ) - const { setValue: setCronJobForEditing, value: cronJobForEditing } = useQueryStateWithSelect({ - urlKey: 'edit', - select: (jobid: string) => { - if (!jobid) return undefined - const job = grid.rows.find((j) => j.jobid.toString() === jobid) - return job - ? { jobname: job.jobname, schedule: job.schedule, active: job.active, command: job.command } - : undefined - }, - enabled: grid.rows.length > 0 && !grid.isLoading, - onError: () => toast.error(`Cron job not found`), - }) + const [cronJobIdForEditing, setCronJobForEditing] = useQueryState('edit', parseAsString) + const cronJobForEditing = grid.rows.find((j) => j.jobid.toString() === cronJobIdForEditing) - const { setValue: setCronJobForDeletion, value: cronJobForDeletion } = useQueryStateWithSelect({ - urlKey: 'delete', - select: (jobid: string) => - jobid ? grid.rows.find((j) => j.jobid.toString() === jobid) : undefined, - enabled: grid.rows.length > 0 && !grid.isLoading, - onError: (_error, selectedId) => - handleErrorOnDelete(deletingCronJobIdRef, selectedId, `Cron job not found`), - }) + const [, setCronJobForDeletion] = useQueryState('delete', parseAsString) const { data: extensions = [] } = useDatabaseExtensionsQuery({ projectRef: project?.ref, @@ -127,12 +114,6 @@ export const CronjobsTab = () => { grid.fetchNextPage() } - // Create job sheet - const [createCronJobSheetShown, setCreateCronJobSheetShown] = useQueryState( - 'new', - parseAsBoolean.withDefault(false).withOptions({ clearOnDefault: true }) - ) - const onOpenCreateJobSheet = () => { sendEvent({ action: 'cron_job_create_clicked', @@ -156,7 +137,6 @@ export const CronjobsTab = () => { createNavigationHandler(url, router)(event) } - const [isDirty, setIsDirty] = useState(false) const onClose = () => { setCronJobForEditing(null) setCreateCronJobSheetShown(false) @@ -170,6 +150,13 @@ export const CronjobsTab = () => { }, }) + useEffect(() => { + if (grid.isSuccess && !!cronJobIdForEditing && !cronJobForEditing) { + toast('Cron job not found') + setCronJobForEditing(null) + } + }, [cronJobForEditing, cronJobIdForEditing, grid.isSuccess, setCronJobForEditing]) + return ( <>
@@ -198,19 +185,7 @@ export const CronjobsTab = () => {
- {cronJobForDeletion && ( - { - deletingCronJobIdRef.current = null - setCronJobForDeletion(null) - }} - onDeleteStart={(jobId) => { - deletingCronJobIdRef.current = jobId - }} - cronJob={cronJobForDeletion} - /> - )} + diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.useCronJobsData.ts b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.useCronJobsData.ts index 1939b8895be9f..02b96e02fc2c4 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.useCronJobsData.ts +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobsTab.useCronJobsData.ts @@ -27,6 +27,7 @@ type UseCronJobsDataParams = ConnectionVars & { interface CronJobsGridState { rows: Array + isSuccess: boolean isLoading: boolean error: ResponseError | null isRefetching: boolean @@ -63,6 +64,7 @@ export function useCronJobsData({ const { data: cronJobsData, error: cronJobsError, + isSuccess: isCronJobsSuccess, isLoading: isCronJobsLoading, isRefetching: isCronJobsRefetching, isFetchingNextPage, @@ -81,6 +83,7 @@ export function useCronJobsData({ const { data: cronJobsMinimalData, error: cronJobsMinimalError, + isSuccess: isCronJobsMinimalSuccess, isLoading: isCronJobsMinimalLoading, isRefetching: isCronJobsMinimalRefetching, isFetchingNextPage: isFetchingNextPageMinimal, @@ -128,6 +131,7 @@ export function useCronJobsData({ grid: { rows: cronJobs, error: useMinimalQuery ? cronJobsMinimalError : cronJobsError, + isSuccess: useMinimalQuery ? isCronJobsMinimalSuccess : isCronJobsSuccess, isLoading: useMinimalQuery ? isCronJobsMinimalLoading : isCronJobsLoading, isRefetching: useMinimalQuery ? isCronJobsMinimalRefetching : isCronJobsRefetching, isFetchingNextPage: useMinimalQuery ? isFetchingNextPageMinimal : isFetchingNextPage, diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/DeleteCronJob.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/DeleteCronJob.tsx index 8fecb0b896f37..4bfd71e4b1ff3 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/DeleteCronJob.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/DeleteCronJob.tsx @@ -1,43 +1,50 @@ -import { parseAsString, useQueryState } from 'nuqs' -import { toast } from 'sonner' - import { TextConfirmModal } from 'components/ui/TextConfirmModalWrapper' import { useDatabaseCronJobDeleteMutation } from 'data/database-cron-jobs/database-cron-jobs-delete-mutation' -import { CronJob } from 'data/database-cron-jobs/database-cron-jobs-infinite-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { cleanPointerEventsNoneOnBody } from 'lib/helpers' +import { parseAsString, useQueryState } from 'nuqs' +import { useEffect } from 'react' +import { toast } from 'sonner' import { ConfirmationModal } from 'ui-patterns/Dialogs/ConfirmationModal' -interface DeleteCronJobProps { - cronJob: CronJob - visible: boolean - onClose: () => void - onDeleteStart?: (jobId: string) => void -} +import { useCronJobsData } from './CronJobsTab.useCronJobsData' -export const DeleteCronJob = ({ cronJob, visible, onClose, onDeleteStart }: DeleteCronJobProps) => { +export const DeleteCronJob = () => { const { data: project } = useSelectedProjectQuery() const { data: org } = useSelectedOrganizationQuery() + const [searchQuery] = useQueryState('search', parseAsString.withDefault('')) + const [cronJobIdForDeletion, setCronJobForDeletion] = useQueryState('delete', parseAsString) + + const { grid } = useCronJobsData({ + projectRef: project?.ref, + connectionString: project?.connectionString, + searchQuery, + }) + const cronJob = grid.rows.find((j) => j.jobid.toString() === cronJobIdForDeletion) const { mutate: sendEvent } = useSendEventMutation() - const { mutate: deleteDatabaseCronJob, isPending } = useDatabaseCronJobDeleteMutation({ + const { + mutate: deleteDatabaseCronJob, + isPending, + isSuccess: isSuccessDelete, + } = useDatabaseCronJobDeleteMutation({ onSuccess: () => { sendEvent({ action: 'cron_job_removed', groups: { project: project?.ref ?? 'Unknown', organization: org?.slug ?? 'Unknown' }, }) - toast.success(`Successfully removed cron job ${cronJob.jobname}`) - onClose() + toast.success(`Successfully removed cron job`) + setCronJobForDeletion(null) }, }) async function handleDelete() { if (!project) return console.error('Project is required') + if (!cronJob) return console.error('Cron job is missing') - onDeleteStart?.(cronJob.jobid.toString()) deleteDatabaseCronJob({ jobId: cronJob.jobid, projectRef: project.ref, @@ -46,6 +53,13 @@ export const DeleteCronJob = ({ cronJob, visible, onClose, onDeleteStart }: Dele }) } + useEffect(() => { + if (grid.isSuccess && !!cronJobIdForDeletion && !cronJob && !isSuccessDelete) { + toast('Cron job not found') + setCronJobForDeletion(null) + } + }, [cronJob, cronJobIdForDeletion, grid.isSuccess, isSuccessDelete, setCronJobForDeletion]) + if (!cronJob) { return null } @@ -55,9 +69,9 @@ export const DeleteCronJob = ({ cronJob, visible, onClose, onDeleteStart }: Dele return ( { - onClose() + setCronJobForDeletion(null) cleanPointerEventsNoneOnBody() }} onConfirm={handleDelete} @@ -72,10 +86,10 @@ export const DeleteCronJob = ({ cronJob, visible, onClose, onDeleteStart }: Dele return ( { - onClose() + setCronJobForDeletion(null) cleanPointerEventsNoneOnBody() }} title="Delete this cron job" diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/AddNewSecretModal.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/AddNewSecretModal.tsx index 82b5995a7e702..208ade316f4c7 100644 --- a/apps/studio/components/interfaces/Integrations/Vault/Secrets/AddNewSecretModal.tsx +++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/AddNewSecretModal.tsx @@ -1,27 +1,27 @@ +import { useVaultSecretCreateMutation } from 'data/vault/vault-secret-create-mutation' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Eye, EyeOff } from 'lucide-react' +import { parseAsBoolean, useQueryState } from 'nuqs' import { useEffect, useState } from 'react' import { toast } from 'sonner' - -import { useVaultSecretCreateMutation } from 'data/vault/vault-secret-create-mutation' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { Button, Form, Input, Modal } from 'ui' -interface AddNewSecretModalProps { - visible: boolean - onClose: () => void -} - -const AddNewSecretModal = ({ visible, onClose }: AddNewSecretModalProps) => { +export const AddNewSecretModal = () => { const { data: project } = useSelectedProjectQuery() const [showSecretValue, setShowSecretValue] = useState(false) const { mutateAsync: addSecret } = useVaultSecretCreateMutation() + const [showAddSecretModal, setShowAddSecretModal] = useQueryState( + 'new', + parseAsBoolean.withDefault(false) + ) + useEffect(() => { - if (visible) { + if (showAddSecretModal) { setShowSecretValue(false) } - }, [visible]) + }, [showAddSecretModal]) const validate = (values: any) => { const errors: any = {} @@ -46,7 +46,7 @@ const AddNewSecretModal = ({ visible, onClose }: AddNewSecretModalProps) => { secret: values.secret, }) toast.success(`Successfully added new secret ${values.name}`) - onClose() + setShowAddSecretModal(null) } catch (error: any) { // [Joshen] No error handler required as they are all handled within the mutations already } finally { @@ -55,7 +55,13 @@ const AddNewSecretModal = ({ visible, onClose }: AddNewSecretModalProps) => { } return ( - + setShowAddSecretModal(null)} + header="Add new secret" + >
{ -
) }, - renderCell: ({ row }) => ( - - ), + renderCell: ({ row }) => , } return result }) diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx index fa68a5142e47d..fa3bbaa251f41 100644 --- a/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx +++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/SecretsManagement.tsx @@ -1,24 +1,20 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { sortBy } from 'lodash' -import { RefreshCw, Search, X } from 'lucide-react' -import { parseAsBoolean, useQueryState } from 'nuqs' -import { useEffect, useMemo, useRef, useState } from 'react' -import DataGrid, { Row } from 'react-data-grid' -import { toast } from 'sonner' - import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import { DocsButton } from 'components/ui/DocsButton' import { useVaultSecretsQuery } from 'data/vault/vault-secrets-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { handleErrorOnDelete, useQueryStateWithSelect } from 'hooks/misc/useQueryStateWithSelect' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { DOCS_URL } from 'lib/constants' +import { sortBy } from 'lodash' +import { RefreshCw, Search, X } from 'lucide-react' +import { parseAsBoolean, useQueryState } from 'nuqs' +import { useEffect, useMemo, useState } from 'react' +import DataGrid, { Row } from 'react-data-grid' import type { VaultSecret } from 'types' import { Button, cn, - Input, LoadingLine, Select_Shadcn_, SelectContent_Shadcn_, @@ -26,23 +22,20 @@ import { SelectTrigger_Shadcn_, SelectValue_Shadcn_, } from 'ui' -import AddNewSecretModal from './AddNewSecretModal' -import DeleteSecretModal from './DeleteSecretModal' -import EditSecretModal from './EditSecretModal' +import { Input } from 'ui-patterns/DataInputs/Input' + +import { AddNewSecretModal } from './AddNewSecretModal' +import { DeleteSecretModal } from './DeleteSecretModal' +import { EditSecretModal } from './EditSecretModal' import { formatSecretColumns } from './Secrets.utils' +import AlertError from '@/components/ui/AlertError' export const SecretsManagement = () => { const { search } = useParams() const { data: project } = useSelectedProjectQuery() - // Track the ID being deleted to exclude it from error checking - const deletingSecretIdRef = useRef(null) - const [searchValue, setSearchValue] = useState('') - const [showAddSecretModal, setShowAddSecretModal] = useQueryState( - 'new', - parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true }) - ) + const [, setShowAddSecretModal] = useQueryState('new', parseAsBoolean.withDefault(false)) const [selectedSort, setSelectedSort] = useState<'updated_at' | 'name'>('updated_at') const { can: canManageSecrets } = useAsyncCheckPermissions( @@ -52,32 +45,17 @@ export const SecretsManagement = () => { const { data, + error, + isError, isPending: isLoading, isRefetching, refetch, - error, - isError, } = useVaultSecretsQuery({ - projectRef: project?.ref!, + projectRef: project?.ref, connectionString: project?.connectionString, }) const allSecrets = useMemo(() => data || [], [data]) - const { setValue: setSelectedSecretToEdit, value: secretToEdit } = useQueryStateWithSelect({ - urlKey: 'edit', - select: (id: string) => (id ? allSecrets?.find((secret) => secret.id === id) : undefined), - enabled: !!allSecrets && !isLoading, - onError: () => toast.error(`Secret not found`), - }) - - const { setValue: setSelectedSecretToRemove, value: secretToDelete } = useQueryStateWithSelect({ - urlKey: 'delete', - select: (id: string) => (id ? allSecrets?.find((secret) => secret.id === id) : undefined), - enabled: !!allSecrets && !isLoading, - onError: (_error, selectedId) => - handleErrorOnDelete(deletingSecretIdRef, selectedId, `Secret not found`), - }) - const secrets = useMemo(() => { const filtered = searchValue.length > 0 @@ -94,19 +72,12 @@ export const SecretsManagement = () => { return sortBy(filtered, (s) => (s.name || '').toLowerCase()) }, [allSecrets, searchValue, selectedSort]) + const columns = useMemo(() => formatSecretColumns(), []) + useEffect(() => { if (search !== undefined) setSearchValue(search) }, [search]) - const columns = useMemo( - () => - formatSecretColumns({ - onSelectEdit: (secret) => setSelectedSecretToEdit(secret.id), - onSelectRemove: (secret) => setSelectedSecretToRemove(secret.id), - }), - [setSelectedSecretToEdit, setSelectedSecretToRemove] - ) - return ( <>
@@ -126,9 +97,7 @@ export const SecretsManagement = () => { size="tiny" type="text" icon={} - onClick={() => { - setSearchValue('') - }} + onClick={() => setSearchValue('')} className="p-0 h-5 w-5" /> ), @@ -183,8 +152,8 @@ export const SecretsManagement = () => { {isError ? ( -
-

Failed to load secrets

+
+
) : ( {
- setShowAddSecretModal(false)} - /> - {secretToEdit && ( - setSelectedSecretToEdit(null)} - /> - )} - { - deletingSecretIdRef.current = secretId - }} - onClose={() => { - deletingSecretIdRef.current = null - setSelectedSecretToRemove(null) - }} - /> + + + + + ) } diff --git a/apps/studio/components/interfaces/Integrations/Vault/Secrets/__tests__/EditSecretModal.test.tsx b/apps/studio/components/interfaces/Integrations/Vault/Secrets/__tests__/EditSecretModal.test.tsx index 497feb435ef8b..cfbf274f925e9 100644 --- a/apps/studio/components/interfaces/Integrations/Vault/Secrets/__tests__/EditSecretModal.test.tsx +++ b/apps/studio/components/interfaces/Integrations/Vault/Secrets/__tests__/EditSecretModal.test.tsx @@ -2,13 +2,14 @@ import { fireEvent, screen, waitFor } from '@testing-library/dom' import userEvent from '@testing-library/user-event' import { ProjectContextProvider } from 'components/layouts/ProjectLayout/ProjectContext' import { mockAnimationsApi } from 'jsdom-testing-mocks' -import { useState } from 'react' -import { render } from 'tests/helpers' +import { HttpResponse } from 'msw' +import { customRender } from 'tests/lib/custom-render' import { addAPIMock } from 'tests/lib/msw' import { routerMock } from 'tests/lib/route-mock' import type { VaultSecret } from 'types' -import { beforeEach, describe, expect, it, vi } from 'vitest' -import EditSecretModal from '../EditSecretModal' +import { beforeEach, describe, expect, it } from 'vitest' + +import { EditSecretModal } from '../EditSecretModal' const secret: VaultSecret = { id: '47ca58b4-01c5-4a71-8814-c73856b02e0e', @@ -19,32 +20,13 @@ const secret: VaultSecret = { updated_at: '2025-07-13 14:51:37.818223+00', } -const Page = ({ onClose }: { onClose: () => void }) => { - const [open, setOpen] = useState(false) - - return ( - - - - { - setOpen(false) - onClose() - }} - /> - - ) -} - mockAnimationsApi() describe(`EditSecretModal`, () => { beforeEach(() => { // useSelectedProjectQuery -> useParams routerMock.setCurrentUrl(`/project/default/integrations/vault/secrets`) - // 'http://localhost:3000/api/platform/projects/default' + // useSelectedProjectQuery addAPIMock({ method: `get`, path: `/platform/projects/:ref`, @@ -60,24 +42,40 @@ describe(`EditSecretModal`, () => { status: 'ACTIVE_HEALTHY', }, }) - // 'http://localhost:3000/api/platform/pg-meta/default/query?key=projects-default-secrets-47ca58b4-01c5-4a71-8814-c73856b02e0e' - // 'http://localhost:3000/api/platform/pg-meta/default/query?key=' - // useVaultSecretDecryptedValueQuery + useVaultSecretUpdateMutation - // both call the same endpoint but execute different SQL queries + // useVaultSecretsQuery + useVaultSecretDecryptedValueQuery + useVaultSecretUpdateMutation + // all call the same endpoint but execute different SQL queries addAPIMock({ method: `post`, path: `/platform/pg-meta/:ref/query`, // @ts-expect-error this path erroneously has a `never` return type when it should be `unknown` since it executes a SQL query - response: [{ decrypted_secret: 'bar', update_secret: '' }], + response: async ({ request }) => { + const body = (await request.json()) as { query: string } + const query = body.query + + if (query.includes('decrypted_secrets')) { + return HttpResponse.json([{ decrypted_secret: 'bar' }]) + } else if (query.includes('update_secret')) { + return HttpResponse.json([{ update_secret: '' }]) + } + // vault.secrets list query + return HttpResponse.json([secret]) + }, }) }) it(`renders a modal pre-filled with the secret's values`, async () => { - const onClose = vi.fn() - render() - - const openButton = screen.getByRole(`button`, { name: `Open` }) - await userEvent.click(openButton) + customRender( + + + , + { + nuqs: { + searchParams: { + edit: secret.id, + }, + }, + } + ) await screen.findByRole(`dialog`) @@ -99,6 +97,8 @@ describe(`EditSecretModal`, () => { fireEvent.click(submitButton) - await waitFor(() => expect(onClose).toHaveBeenCalledOnce()) + await waitFor(() => { + expect(screen.queryByRole(`dialog`)).not.toBeInTheDocument() + }) }) }) diff --git a/apps/studio/components/interfaces/Integrations/Webhooks/ListTab.tsx b/apps/studio/components/interfaces/Integrations/Webhooks/ListTab.tsx index 9c74948e71307..e94a39912eb0d 100644 --- a/apps/studio/components/interfaces/Integrations/Webhooks/ListTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Webhooks/ListTab.tsx @@ -1,96 +1,26 @@ -import { PostgresTrigger } from '@supabase/postgres-meta' import { PermissionAction } from '@supabase/shared-types/out/constants' -import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs' -import { useMemo, useRef } from 'react' -import DeleteHookModal from '@/components/interfaces/Database/Hooks/DeleteHookModal' +import { DeleteHookModal } from '@/components/interfaces/Database/Hooks/DeleteHookModal' import { EditHookPanel } from '@/components/interfaces/Database/Hooks/EditHookPanel' import { HooksList } from '@/components/interfaces/Database/Hooks/HooksList/HooksList' -import NoPermission from '@/components/ui/NoPermission' -import { useDatabaseHooksQuery } from '@/data/database-triggers/database-triggers-query' +import { NoPermission } from '@/components/ui/NoPermission' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' -import { handleErrorOnDelete, useQueryStateWithSelect } from '@/hooks/misc/useQueryStateWithSelect' -import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' export const WebhooksListTab = () => { - const { data: project } = useSelectedProjectQuery() - - // Track the ID being deleted to exclude it from error checking - const deletingHookIdRef = useRef(null) - - const [showCreateHookForm, setShowCreateHookForm] = useQueryState( - 'new', - parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true }) - ) - - const [selectedHookIdToEdit, setSelectedHookIdToEdit] = useQueryState( - 'edit', - parseAsString.withDefault('').withOptions({ history: 'push', clearOnDefault: true }) - ) - const { can: canReadWebhooks, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions( PermissionAction.TENANT_SQL_ADMIN_READ, 'triggers' ) - const { data: hooks, isPending: isLoadingHooks } = useDatabaseHooksQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - }) - - const { setValue: setSelectedHookToDelete, value: selectedHookToDelete } = - useQueryStateWithSelect({ - urlKey: 'delete', - select: (id: string) => (id ? hooks?.find((hook) => hook.id.toString() === id) : undefined), - enabled: !!hooks && !isLoadingHooks, - onError: (_error, selectedId) => - handleErrorOnDelete(deletingHookIdRef, selectedId, `Webhook not found`), - }) - - const createHook = () => { - setShowCreateHookForm(true) - } - - const editHook = (hook: PostgresTrigger) => { - setSelectedHookIdToEdit(hook.id.toString()) - } - - const deleteHook = (hook: PostgresTrigger) => { - setSelectedHookToDelete(hook.id.toString()) - } - - const selectedHookToEdit = useMemo( - () => hooks?.find((hook) => hook.id.toString() === selectedHookIdToEdit), - [hooks, selectedHookIdToEdit] - ) - if (isPermissionsLoaded && !canReadWebhooks) { return } return (
- - { - setShowCreateHookForm(false) - setSelectedHookIdToEdit('') - }} - /> - { - deletingHookIdRef.current = null - setSelectedHookToDelete(null) - }} - onDeleteStart={(hookId: string) => { - deletingHookIdRef.current = hookId - }} - /> + + +
) } diff --git a/apps/studio/components/interfaces/Integrations/Webhooks/OverviewTab.tsx b/apps/studio/components/interfaces/Integrations/Webhooks/OverviewTab.tsx index ce58949c52fc3..be57417d2946d 100644 --- a/apps/studio/components/interfaces/Integrations/Webhooks/OverviewTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Webhooks/OverviewTab.tsx @@ -1,6 +1,4 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { toast } from 'sonner' - import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import NoPermission from 'components/ui/NoPermission' @@ -8,8 +6,10 @@ import { useHooksEnableMutation } from 'data/database/hooks-enable-mutation' import { useSchemasQuery } from 'data/database/schemas-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { toast } from 'sonner' import { Admonition } from 'ui-patterns' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' + import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab' export const WebhooksOverviewTab = () => { @@ -73,7 +73,7 @@ export const WebhooksOverviewTab = () => { HTTP endpoint

enableHooksForProject()} disabled={isEnablingHooks} tooltip={{ diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/DeleteWrapperModal.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/DeleteWrapperModal.tsx index 486ba091f73a3..dd5268c1b4573 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/DeleteWrapperModal.tsx +++ b/apps/studio/components/interfaces/Integrations/Wrappers/DeleteWrapperModal.tsx @@ -1,33 +1,47 @@ -import { MutableRefObject } from 'react' -import { toast } from 'sonner' -import { Modal } from 'ui' - +import { useParams } from 'common' import { useFDWDeleteMutation } from 'data/fdw/fdw-delete-mutation' -import type { FDW } from 'data/fdw/fdws-query' +import { useFDWsQuery } from 'data/fdw/fdws-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { getWrapperMetaForWrapper } from './Wrappers.utils' +import { parseAsString, useQueryState } from 'nuqs' +import { useEffect, useMemo } from 'react' +import { toast } from 'sonner' +import { Modal } from 'ui' -interface DeleteWrapperModalProps { - selectedWrapper?: FDW - onClose: () => void - deletingWrapperIdRef?: MutableRefObject -} +import { INTEGRATIONS } from '../Landing/Integrations.constants' +import { getWrapperMetaForWrapper, wrapperMetaComparator } from './Wrappers.utils' -const DeleteWrapperModal = ({ - selectedWrapper, - onClose, - deletingWrapperIdRef, -}: DeleteWrapperModalProps) => { +export const DeleteWrapperModal = () => { + const { id, ref } = useParams() const { data: project } = useSelectedProjectQuery() - const { mutate: deleteFDW, isPending: isDeleting } = useFDWDeleteMutation({ + const integration = INTEGRATIONS.find((i) => i.id === id) + + const { data, isSuccess } = useFDWsQuery({ + projectRef: ref, + connectionString: project?.connectionString, + }) + + const wrappers = useMemo( + () => + integration && integration.type === 'wrapper' && data + ? data.filter((wrapper) => wrapperMetaComparator(integration.meta, wrapper)) + : [], + [data, integration] + ) + + const [selectedWrapperIdToDelete, setSelectedWrapperToDelete] = useQueryState( + 'delete', + parseAsString + ) + const selectedWrapper = wrappers.find((x) => x.id.toString() === selectedWrapperIdToDelete) + + const { + mutate: deleteFDW, + isPending: isDeleting, + isSuccess: isSuccessDelete, + } = useFDWDeleteMutation({ onSuccess: () => { toast.success(`Successfully disabled ${selectedWrapper?.name} foreign data wrapper`) - onClose() - }, - onError: () => { - if (deletingWrapperIdRef) { - deletingWrapperIdRef.current = null - } + setSelectedWrapperToDelete(null) }, }) const wrapperMeta = getWrapperMetaForWrapper(selectedWrapper) @@ -37,10 +51,6 @@ const DeleteWrapperModal = ({ 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, @@ -49,13 +59,27 @@ const DeleteWrapperModal = ({ }) } + useEffect(() => { + if (isSuccess && !!selectedWrapperIdToDelete && !selectedWrapper && !isSuccessDelete) { + toast('Wrapper not found') + setSelectedWrapperToDelete(null) + } + }, [ + isSuccess, + isSuccessDelete, + selectedWrapper, + selectedWrapperIdToDelete, + setSelectedWrapperToDelete, + ]) + return ( onClose()} + onCancel={() => setSelectedWrapperToDelete(null)} onConfirm={() => onConfirmDelete()} header={`Confirm to disable ${selectedWrapper?.name}`} > @@ -68,5 +92,3 @@ const DeleteWrapperModal = ({ ) } - -export default DeleteWrapperModal diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx index e31881d17523d..3373c8631687a 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx +++ b/apps/studio/components/interfaces/Integrations/Wrappers/WrapperRow.tsx @@ -1,53 +1,30 @@ 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 { MutableRefObject, useState } from 'react' - import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' import type { FDW } from 'data/fdw/fdws-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' -import { - Badge, - Sheet, - SheetContent, - TableCell, - TableRow, - Tooltip, - TooltipContent, - TooltipTrigger, -} from 'ui' +import { partition } from 'lodash' +import { ChevronRight, Edit, ExternalLink, Table2, Trash } from 'lucide-react' +import Link from 'next/link' +import { parseAsString, useQueryState } from 'nuqs' +import { Badge, TableCell, TableRow, Tooltip, TooltipContent, TooltipTrigger } from 'ui' + import { INTEGRATIONS } from '../Landing/Integrations.constants' -import DeleteWrapperModal from './DeleteWrapperModal' -import { EditWrapperSheet } from './EditWrapperSheet' import { convertKVStringArrayToJson, formatWrapperTables } from './Wrappers.utils' interface WrapperRowProps { wrapper: FDW - selectedWrapperToEdit?: FDW - selectedWrapperToDelete?: FDW - setSelectedWrapperToEdit: (value: string | null) => void - setSelectedWrapperToDelete: (value: string | null) => void - deletingWrapperIdRef: MutableRefObject } -export const WrapperRow = ({ - wrapper, - selectedWrapperToEdit, - selectedWrapperToDelete, - setSelectedWrapperToEdit, - setSelectedWrapperToDelete, - deletingWrapperIdRef, -}: WrapperRowProps) => { +export const WrapperRow = ({ wrapper }: WrapperRowProps) => { const { ref, id } = useParams() const { can: canManageWrappers } = useAsyncCheckPermissions( PermissionAction.TENANT_SQL_ADMIN_WRITE, 'wrappers' ) - const editWrapperShown = selectedWrapperToEdit?.id === wrapper.id - const [isClosingEditWrapper, setIsClosingEditWrapper] = useState(false) + const [, setSelectedWrapperToEdit] = useQueryState('edit', parseAsString) + const [, setSelectedWrapperToDelete] = useQueryState('delete', parseAsString) const integration = INTEGRATIONS.find((i) => i.id === id) @@ -64,152 +41,118 @@ export const WrapperRow = ({ const _tables = formatWrapperTables(wrapper, integration?.meta) return ( - <> - - - {wrapper.name} + + + {wrapper.name} - {visibleMetadata.map((metadata) => ( -
- {metadata.label}: - - {serverOptions[metadata.name]} - -
- ))} -
+ {visibleMetadata.map((metadata) => ( +
+ {metadata.label}: + + {serverOptions[metadata.name]} + +
+ ))} +
- - {_tables?.map((table) => { - const target = table.table ?? table.object ?? table.src_key + + {_tables?.map((table) => { + const target = table.table ?? table.object ?? table.src_key - return ( -
- -
- {integration.icon({ className: 'p-0' })} -
+ return ( +
+ +
+ {integration.icon({ className: 'p-0' })} +
+ + {target} + + {target} + + + +
+ + + + - {target} + + {table.schema}.{table.table_name} + - {target} + {table.schema}.{table.table_name} - - - - - - - - {table.schema}.{table.table_name} - - - {table.schema}.{table.table_name} - - - - -
- ) - })} - - - {encryptedMetadata.map((metadata) => ( -
- - - {metadata.label} - -
- -
- ))} -
- -
- } - className="px-1.5" - onClick={() => setSelectedWrapperToEdit(wrapper.id.toString())} - tooltip={{ - content: { - side: 'bottom', - text: !canManageWrappers - ? 'You need additional permissions to edit wrappers' - : 'Edit wrapper', - }, - }} - /> - } - className="px-1.5" - onClick={() => setSelectedWrapperToDelete(wrapper.id.toString())} - tooltip={{ - content: { - side: 'bottom', - text: !canManageWrappers - ? 'You need additional permissions to delete wrappers' - : 'Delete wrapper', - }, - }} - /> + ) + })} + + + {encryptedMetadata.map((metadata) => ( +
+ + + {metadata.label} + +
+ +
+
-
- - { - if (!open) { - setIsClosingEditWrapper(true) - } - }} - > - - {selectedWrapperToEdit && ( - { - setSelectedWrapperToEdit(null) - setIsClosingEditWrapper(false) - }} - isClosing={isClosingEditWrapper} - setIsClosing={setIsClosingEditWrapper} - /> - )} - - - {selectedWrapperToDelete && ( - setSelectedWrapperToDelete(null)} - deletingWrapperIdRef={deletingWrapperIdRef} - /> - )} - + ))} + + +
+ } + className="px-1.5" + onClick={() => setSelectedWrapperToEdit(wrapper.id.toString())} + tooltip={{ + content: { + side: 'bottom', + text: !canManageWrappers + ? 'You need additional permissions to edit wrappers' + : 'Edit wrapper', + }, + }} + /> + } + className="px-1.5" + onClick={() => setSelectedWrapperToDelete(wrapper.id.toString())} + tooltip={{ + content: { + side: 'bottom', + text: !canManageWrappers + ? 'You need additional permissions to delete wrappers' + : 'Delete wrapper', + }, + }} + /> +
+
+ ) } diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/WrapperTable.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/WrapperTable.tsx index 431fdf15aa67d..0c4ee61aa276d 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/WrapperTable.tsx +++ b/apps/studio/components/interfaces/Integrations/Wrappers/WrapperTable.tsx @@ -1,13 +1,14 @@ -import { useMemo, useRef } from 'react' -import { toast } from 'sonner' - import { useParams } from 'common' import { useFDWsQuery } from 'data/fdw/fdws-query' -import { handleErrorOnDelete, useQueryStateWithSelect } from 'hooks/misc/useQueryStateWithSelect' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { parseAsString, useQueryState } from 'nuqs' +import { useEffect, useMemo, useState } from 'react' +import { toast } from 'sonner' import { Card, cn, + Sheet, + SheetContent, Table, TableBody, TableCell, @@ -16,7 +17,10 @@ import { TableHeader, TableRow, } from 'ui' + import { INTEGRATIONS } from '../Landing/Integrations.constants' +import { DeleteWrapperModal } from './DeleteWrapperModal' +import { EditWrapperSheet } from './EditWrapperSheet' import { WrapperRow } from './WrapperRow' import { wrapperMetaComparator } from './Wrappers.utils' @@ -29,7 +33,9 @@ export const WrapperTable = ({ isLatest = false }: WrapperTableProps) => { const { data: project } = useSelectedProjectQuery() const integration = INTEGRATIONS.find((i) => i.id === id) - const { data } = useFDWsQuery({ + const [isClosingEditWrapper, setIsClosingEditWrapper] = useState(false) + + const { data, isSuccess } = useFDWsQuery({ projectRef: ref, connectionString: project?.connectionString, }) @@ -42,27 +48,15 @@ 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 [selectedWrapperIdToEdit, setSelectedWrapperToEdit] = useQueryState('edit', parseAsString) + const selectedWrapperToEdit = wrappers.find((w) => w.id.toString() === selectedWrapperIdToEdit) - 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`), - }) + useEffect(() => { + if (isSuccess && !!selectedWrapperIdToEdit && !selectedWrapperToEdit) { + toast('Wrapper not found') + setSelectedWrapperToEdit(null) + } + }, [isSuccess, selectedWrapperIdToEdit, selectedWrapperToEdit, setSelectedWrapperToEdit]) if (!integration || integration.type !== 'wrapper') { return ( @@ -73,50 +67,66 @@ export const WrapperTable = ({ isLatest = false }: WrapperTableProps) => { } return ( - -
- - - Name - Tables - Encrypted key - - Actions - - - - - {(isLatest ? wrappers.slice(0, 3) : wrappers).map((x) => { - return ( - - ) - })} - - tr>td]:hover:bg-inherit', - // Conditionally remove the border-top if there are no wrappers - wrappers.length === 0 ? 'border-t-0' : '' + <> + +
+ + + Name + Tables + Encrypted key + + Actions + + + + + {(isLatest ? wrappers.slice(0, 3) : wrappers).map((x) => { + return + })} + + tr>td]:hover:bg-inherit', + // Conditionally remove the border-top if there are no wrappers + wrappers.length === 0 ? 'border-t-0' : '' + )} + > + + + {wrappers.length} {integration?.name} + {wrappers.length === 0 || wrappers.length > 1 ? 's' : ''} created + + + +
+ + + { + if (!open) setIsClosingEditWrapper(true) + }} + > + + {selectedWrapperToEdit && ( + { + setSelectedWrapperToEdit(null) + setIsClosingEditWrapper(false) + }} + isClosing={isClosingEditWrapper} + setIsClosing={setIsClosingEditWrapper} + /> )} - > - - - {wrappers.length} {integration?.name} - {wrappers.length > 1 ? 's' : ''} created - - - - - + + + + + ) } diff --git a/apps/studio/components/interfaces/Integrations/Wrappers/WrappersTab.tsx b/apps/studio/components/interfaces/Integrations/Wrappers/WrappersTab.tsx index d3e7f9e55667d..50eacdefe4c70 100644 --- a/apps/studio/components/interfaces/Integrations/Wrappers/WrappersTab.tsx +++ b/apps/studio/components/interfaces/Integrations/Wrappers/WrappersTab.tsx @@ -1,16 +1,15 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { HTMLProps, ReactNode, useCallback, useState } from 'react' - import { useParams } from 'common' import { ButtonTooltip } from 'components/ui/ButtonTooltip' -import { FDW, useFDWsQuery } from 'data/fdw/fdws-query' +import { useFDWsQuery } from 'data/fdw/fdws-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose' +import { HTMLProps, ReactNode, useCallback, useState } from 'react' import { Sheet, SheetContent } from 'ui' import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal' + import { CreateWrapperSheet } from './CreateWrapperSheet' -import DeleteWrapperModal from './DeleteWrapperModal' import { WRAPPERS } from './Wrappers.constants' import { wrapperMetaComparator } from './Wrappers.utils' import { WrapperTable } from './WrapperTable' @@ -18,7 +17,6 @@ import { WrapperTable } from './WrapperTable' export const WrappersTab = () => { const { id } = useParams() const { data: project } = useSelectedProjectQuery() - const [selectedWrapperForDelete, setSelectedWrapperForDelete] = useState(null) const [createWrapperShown, setCreateWrapperShown] = useState(false) const { can: canCreateWrapper } = useAsyncCheckPermissions( @@ -104,12 +102,6 @@ export const WrappersTab = () => { return ( - {selectedWrapperForDelete && ( - setSelectedWrapperForDelete(null)} - /> - )} ) diff --git a/apps/studio/data/vault/keys.ts b/apps/studio/data/vault/keys.ts index 1a01eb9c7072a..2b9ab9ceb00e2 100644 --- a/apps/studio/data/vault/keys.ts +++ b/apps/studio/data/vault/keys.ts @@ -1,5 +1,5 @@ export const vaultSecretsKeys = { list: (projectRef: string | undefined) => ['projects', projectRef, 'secrets'] as const, - getDecryptedValue: (projectRef: string | undefined, id: string) => - ['projects', projectRef, 'secrets', id] as const, + getDecryptedValue: (projectRef: string | undefined, id: string | undefined) => + ['projects', projectRef, 'secrets', id].filter(Boolean), } diff --git a/apps/studio/data/vault/vault-secret-decrypted-value-query.ts b/apps/studio/data/vault/vault-secret-decrypted-value-query.ts index 1b4ab8d363353..6c628068fe0e6 100644 --- a/apps/studio/data/vault/vault-secret-decrypted-value-query.ts +++ b/apps/studio/data/vault/vault-secret-decrypted-value-query.ts @@ -1,6 +1,7 @@ import { Query } from '@supabase/pg-meta/src/query' import { useQuery } from '@tanstack/react-query' import { UseCustomQueryOptions } from 'types' + import { executeSql } from '../sql/execute-sql-query' import { vaultSecretsKeys } from './keys' @@ -27,13 +28,15 @@ const vaultSecretDecryptedValuesQuery = (ids: string[]) => { export type VaultSecretsDecryptedValueVariables = { projectRef?: string connectionString?: string | null - id: string + id?: string } export const getDecryptedValue = async ( { projectRef, connectionString, id }: VaultSecretsDecryptedValueVariables, signal?: AbortSignal ) => { + if (!id) throw new Error('ID is required') + const sql = vaultSecretDecryptedValueQuery(id) const { result } = await executeSql( {