From 3d10f2cab90e2a028d41d885476f024bda270e3e Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Mon, 29 Jun 2026 17:43:38 +0800 Subject: [PATCH] Add user flow for iceberg wrapper if api keys are rotated (#47336) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Context We found an issue regarding Analytics Buckets and the Iceberg wrapper - upon creation of an analytics bucket, the wrapper is automatically created for the users which involves using the project's API keys as the catalog's token. However, if the user were to rotate the API keys, this will cause the wrapper to break and there's currently no clear user flow for the user to self-remediate - the only indicator they'll see is just a 403 error (e.g when trying to view the analytics bucket table via FDW on the table editor or SQL editor) ## Changes involved Am adding a user path for users to self-remediate a little, starting from the Table Editor - we'll add a contextual error message as such if we detect a 403 that's caused by an invalid token: Screenshot 2026-06-26 at 17 33 11 We'll subsequently also float this issue up in the Analytics Bucket UI (which is linked from the contextual error above) Screenshot 2026-06-26 at 17 31 52 And users can then choose to use another API key as the catalog token Screenshot 2026-06-26 at 17 31 56 The warning will thereafter go away, and users will be able to query the FDW again via Table Editor or SQL Editor ## To test - [ ] Create an analytics bucket, set up a table and foreign schema (via Query via Postgres) - [ ] Insert some data, or verify that you can view the iceberg table from the Table Editor - [ ] Now rotate your API secret key (delete the old, create a new) - [ ] Verify that you'll run into that error if you view the iceberg table from the Table Editor - [ ] Follow the flow -> Go to the Analytics Bucket UI to update the catalog token - [ ] Verify that thereafter, you can view the iceberg table again from the Table Editor ## Summary by CodeRabbit * **New Features** * Added clearer Iceberg/analytics bucket setup prompts for missing, outdated, or uninstalled wrappers. * Added an “Update catalog token” dialog and a collapsible “View error” troubleshooting UI. * **Bug Fixes** * Improved detection of Iceberg authorization failures and now shows a more specific error with guidance. * Warn users when the saved catalog token no longer matches available API keys. * Enhanced post-update refresh behavior so updated token values display correctly. * **Documentation** * Clarified vault token description to indicate it may be a secret or service role key. --- .../grid/components/grid/GridError.tsx | 53 +++- .../AnalyticsBucketDetails.constants.ts | 2 +- .../AnalyticsBucketDetails/BucketCallouts.tsx | 136 +++++++++++ .../SimpleConfigurationDetails.tsx | 38 +++ .../UpdateCatalogTokenDialog.tsx | 147 +++++++++++ .../AnalyticsBucketDetails/index.tsx | 228 ++---------------- .../vault/vault-secret-update-mutation.ts | 11 +- 7 files changed, 398 insertions(+), 217 deletions(-) create mode 100644 apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/BucketCallouts.tsx create mode 100644 apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/UpdateCatalogTokenDialog.tsx diff --git a/apps/studio/components/grid/components/grid/GridError.tsx b/apps/studio/components/grid/components/grid/GridError.tsx index d18894c8208..368a2d1d456 100644 --- a/apps/studio/components/grid/components/grid/GridError.tsx +++ b/apps/studio/components/grid/components/grid/GridError.tsx @@ -1,13 +1,14 @@ import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'common' +import { ChevronDown } from 'lucide-react' import { useCallback } from 'react' -import { Button } from 'ui' +import { Button, Collapsible, CollapsibleContent, CollapsibleTrigger } from 'ui' import { Admonition } from 'ui-patterns' import { isFilterRelatedError } from './GridError.utils' import { useTableFilter } from '@/components/grid/hooks/useTableFilter' import { useTableSort } from '@/components/grid/hooks/useTableSort' -import AlertError from '@/components/ui/AlertError' +import { AlertError } from '@/components/ui/AlertError' import { HighCostError } from '@/components/ui/HighQueryCost' import { InlineLink } from '@/components/ui/InlineLink' import { ENTITY_TYPE } from '@/data/entity-types/entity-type-constants' @@ -53,6 +54,12 @@ export const GridError = ({ error }: { error?: ResponseError | null }) => { const isForeignTableMissingVaultKeyError = isForeignTable && error?.message?.includes('query vault failed') + const isIcebergUnauthorizedError = + isForeignTable && + error?.message.includes('iceberg error') && + error?.message.includes('403 Forbidden') && + error?.message.includes('Invalid Compact JWS') + const hasActiveFilters = filters.length > 0 const hasFilterRelatedError = hasActiveFilters && isFilterRelatedError(error?.message) @@ -79,6 +86,8 @@ export const GridError = ({ error }: { error?: ResponseError | null }) => { return } else if (isInvalidOrderingOperatorError) { return + } else if (isIcebergUnauthorizedError) { + return } return @@ -165,6 +174,30 @@ const InvalidOrderingOperatorError = ({ error }: { error: ResponseError }) => { ) } +const IcebergUnauthorizedError = ({ error }: { error: ResponseError }) => { + const { ref } = useParams() + + return ( + +

+ The API key from your project that's used to retrieve data from your foreign table is either + incorrect or missing. Verify the API key (token) in your{' '} + Iceberg Bucket. + Alternatively, you can also verify the token value in your{' '} + + wrapper's settings + {' '} + or in Vault. +

+ +
+ ) +} + const GeneralError = ({ error }: { error: ResponseError }) => { const { filters } = useTableFilter() @@ -183,3 +216,19 @@ const GeneralError = ({ error }: { error: ResponseError }) => { ) } + +const ExpandError = ({ error }: { error: ResponseError }) => { + return ( + + +
+ View error + +
+
+ + {error.message} + +
+ ) +} diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/AnalyticsBucketDetails.constants.ts b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/AnalyticsBucketDetails.constants.ts index fe4274afc4d..cb103ee81de 100644 --- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/AnalyticsBucketDetails.constants.ts +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/AnalyticsBucketDetails.constants.ts @@ -19,7 +19,7 @@ export const LABELS: Record = { export const DESCRIPTIONS: Record = { vault_aws_access_key_id: 'Matches the AWS access key ID from an S3 access key.', vault_aws_secret_access_key: 'Matches the AWS secret access from an S3 access key.', - vault_token: 'Corresponds to the service role key.', + vault_token: 'Corresponds to the secret or service role key.', warehouse: 'Matches the name of this bucket.', 's3.endpoint': '', catalog_uri: '', diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/BucketCallouts.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/BucketCallouts.tsx new file mode 100644 index 00000000000..3876dbf344b --- /dev/null +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/BucketCallouts.tsx @@ -0,0 +1,136 @@ +import Link from 'next/link' +import { Button } from 'ui' +import { Admonition } from 'ui-patterns/admonition' + +import { SimpleConfigurationDetails } from './SimpleConfigurationDetails' +import { WrapperMeta } from '@/components/interfaces/Integrations/Wrappers/Wrappers.types' +import { ScaffoldSection } from '@/components/layouts/Scaffold' +import { InlineLink } from '@/components/ui/InlineLink' +import { DatabaseExtension } from '@/data/database-extensions/database-extensions-query' +import { useIcebergWrapperCreateMutation } from '@/data/storage/iceberg-wrapper-create-mutation' +import { DOCS_URL } from '@/lib/constants' +import { isLessThan } from '@/lib/semver' + +export const ExtensionNotInstalled = ({ + bucketName, + projectRef, + wrapperMeta, + wrappersExtension, +}: { + bucketName?: string + projectRef: string + wrapperMeta: WrapperMeta + wrappersExtension: DatabaseExtension +}) => { + const databaseNeedsUpgrading = isLessThan( + wrappersExtension?.default_version ?? '', + wrapperMeta?.minimumExtensionVersion ?? '' + ) + + return ( + <> + + +

+ The Wrappers extension is required in order to query analytics tables.{' '} + {databaseNeedsUpgrading && + 'Please first upgrade your database and then install the extension.'}{' '} + + Learn more + +

+ +
+
+ + + ) +} + +export const ExtensionNeedsUpgrade = ({ + bucketName, + projectRef, + wrapperMeta, + wrappersExtension, +}: { + bucketName?: string + projectRef: string + wrapperMeta: WrapperMeta + wrappersExtension: DatabaseExtension +}) => { + // [Joshen] Default version is what's on the DB, so if the installed version is already the default version + // but still doesnt meet the minimum extension version, then DB upgrade is required + const databaseNeedsUpgrading = + wrappersExtension?.installed_version === wrappersExtension?.default_version + + return ( + <> + + +

+ The {wrapperMeta.label} wrapper requires a minimum extension version of{' '} + {wrapperMeta.minimumExtensionVersion}. You have version{' '} + {wrappersExtension?.installed_version} installed. Please{' '} + {databaseNeedsUpgrading && 'first upgrade your database, and then '}update the extension + by disabling and enabling the Wrappers extension. +

+

+ Before reinstalling the wrapper extension, you must first remove all existing wrappers. + Afterward, you can recreate the wrappers. +

+ +
+
+ + + ) +} + +export const WrapperMissing = ({ bucketName }: { bucketName?: string }) => { + const { mutateAsync: createIcebergWrapper, isPending: isCreatingIcebergWrapper } = + useIcebergWrapperCreateMutation() + + const onSetupWrapper = async () => { + if (!bucketName) return console.error('Bucket name is required') + await createIcebergWrapper({ bucketName }) + } + + return ( + <> + + +

The Iceberg Wrapper integration is required in order to query analytics tables.

+ +
+
+ + + ) +} diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/SimpleConfigurationDetails.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/SimpleConfigurationDetails.tsx index 386def2c402..47e01441a28 100644 --- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/SimpleConfigurationDetails.tsx +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/SimpleConfigurationDetails.tsx @@ -1,8 +1,11 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' import { Card } from 'ui' +import { Admonition } from 'ui-patterns' import { DESCRIPTIONS, LABELS, OPTION_ORDER } from './AnalyticsBucketDetails.constants' import { CopyEnvButton } from './CopyEnvButton' import { DecryptedReadOnlyInput } from './DecryptedReadOnlyInput' +import { UpdateCatalogTokenDialog } from './UpdateCatalogTokenDialog' import { useAnalyticsBucketWrapperInstance } from './useAnalyticsBucketWrapperInstance' import { INTEGRATIONS } from '@/components/interfaces/Integrations/Landing/Integrations.constants' import { WrapperMeta } from '@/components/interfaces/Integrations/Wrappers/Wrappers.types' @@ -14,9 +17,16 @@ import { ScaffoldSectionTitle, } from '@/components/layouts/Scaffold' import { InlineLink } from '@/components/ui/InlineLink' +import { useAPIKeys } from '@/data/api-keys/api-keys-query' +import { useVaultSecretDecryptedValueQuery } from '@/data/vault/vault-secret-decrypted-value-query' +import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' +import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { DOCS_URL } from '@/lib/constants' export const SimpleConfigurationDetails = ({ bucketName }: { bucketName?: string }) => { + const { data: project } = useSelectedProjectQuery() + const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') + const integration = INTEGRATIONS.find((i) => i.id === 'iceberg_wrapper' && i.type === 'wrapper') const wrapperMeta = (integration?.type === 'wrapper' && integration.meta) as WrapperMeta @@ -24,6 +34,23 @@ export const SimpleConfigurationDetails = ({ bucketName }: { bucketName?: string const { data: wrapperInstance } = useAnalyticsBucketWrapperInstance({ bucketId: bucketName }) const wrapperValues = convertKVStringArrayToJson(wrapperInstance?.server_options ?? []) + const { data: decryptedToken, isSuccess: isSuccessVaultDecrypt } = + useVaultSecretDecryptedValueQuery( + { + projectRef: project?.ref, + connectionString: project?.connectionString, + id: wrapperValues?.vault_token, + }, + { enabled: canReadAPIKeys } + ) + + const { data: apiKeysData, isSuccess: isSuccessApiKeys } = useAPIKeys( + { projectRef: project?.ref, reveal: true }, + { enabled: canReadAPIKeys } + ) + + const isTokenValid = apiKeysData?.allSecretKeys.some((x) => x.api_key === decryptedToken) + if (!wrapperInstance) return null return ( @@ -48,6 +75,17 @@ export const SimpleConfigurationDetails = ({ bucketName }: { bucketName?: string /> + {isSuccessApiKeys && isSuccessVaultDecrypt && !isTokenValid && ( + +

+ The Iceberg wrapper's catalog token doesn't match with any of your project's API keys, + and hence authorization will fail when connecting to your bucket. Update the catalog + token to use any of your project's API keys. +

+ {canReadAPIKeys && } +
+ )} + {wrapperMeta.server.options .filter((option) => !option.hidden && wrapperValues[option.name]) diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/UpdateCatalogTokenDialog.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/UpdateCatalogTokenDialog.tsx new file mode 100644 index 00000000000..64e4d1d4d74 --- /dev/null +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/UpdateCatalogTokenDialog.tsx @@ -0,0 +1,147 @@ +import { PermissionAction } from '@supabase/shared-types/out/constants' +import { useQueryClient } from '@tanstack/react-query' +import { useParams } from 'common' +import { useEffect, useState } from 'react' +import { toast } from 'sonner' +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogSection, + DialogSectionSeparator, + DialogTitle, + DialogTrigger, + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from 'ui' +import { Admonition, ShimmeringLoader } from 'ui-patterns' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' + +import { InlineLink } from '@/components/ui/InlineLink' +import { useAPIKeys } from '@/data/api-keys/api-keys-query' +import { fdwKeys } from '@/data/fdw/keys' +import { vaultSecretsKeys } from '@/data/vault/keys' +import { useVaultSecretUpdateMutation } from '@/data/vault/vault-secret-update-mutation' +import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' +import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' + +export const UpdateCatalogTokenDialog = ({ vaultTokenId }: { vaultTokenId?: string }) => { + const { ref } = useParams() + const queryClient = useQueryClient() + const { data: project } = useSelectedProjectQuery() + const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') + + const [open, setOpen] = useState(false) + const [selectedKey, setSelectedKey] = useState() + + const { data: apiKeysData, isPending } = useAPIKeys( + { projectRef: ref, reveal: true }, + { enabled: canReadAPIKeys } + ) + const { allSecretKeys } = apiKeysData ?? {} + + const { mutate: updateVaultSecret, isPending: isUpdating } = useVaultSecretUpdateMutation({ + onSuccess: async () => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: fdwKeys.list(ref) }), + queryClient.invalidateQueries({ + queryKey: vaultSecretsKeys.getDecryptedValue(ref, vaultTokenId), + }), + ]) + + toast.success('Successfully updated catalog token!') + setOpen(false) + }, + }) + + const onSubmit = () => { + if (!project) return console.error('Project is required') + if (!vaultTokenId) return toast.error('ID of catalog token is missing') + + updateVaultSecret({ + projectRef: project?.ref, + connectionString: project?.connectionString, + id: vaultTokenId, + secret: selectedKey, + skipClearCache: true, + }) + } + + useEffect(() => { + if (allSecretKeys?.length) setSelectedKey(allSecretKeys[0].api_key) + }, [allSecretKeys]) + + return ( + + + + + + + Update Iceberg wrapper catalog token + + + + {allSecretKeys?.length === 0 && ( + +

+ Create an API secret key from your{' '} + project's settings{' '} + first +

+
+ )} + + + {isPending ? ( + + ) : ( + + )} + + + + + + +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/index.tsx b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/index.tsx index 3691ec70213..20331cd031e 100644 --- a/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/index.tsx +++ b/apps/studio/components/interfaces/Storage/AnalyticsBuckets/AnalyticsBucketDetails/index.tsx @@ -1,18 +1,17 @@ import { useParams } from 'common' import { uniq } from 'lodash' import { Loader2 } from 'lucide-react' -import Link from 'next/link' import { useRouter } from 'next/router' import { parseAsBoolean, useQueryState } from 'nuqs' import { useEffect, useMemo, useState } from 'react' import { Button, Card, CardContent } from 'ui' import { EmptyStatePresentational } from 'ui-patterns' -import { Admonition } from 'ui-patterns/admonition' import { GenericTableLoader } from 'ui-patterns/ShimmeringLoader' import { DeleteAnalyticsBucketModal } from '../DeleteAnalyticsBucketModal' import { useSelectedAnalyticsBucket } from '../useSelectedAnalyticsBucket' import { HIDE_REPLICATION_USER_FLOW } from './AnalyticsBucketDetails.constants' +import { ExtensionNeedsUpgrade, ExtensionNotInstalled, WrapperMissing } from './BucketCallouts' import { BucketHeader } from './BucketHeader' import { CreateTableInstructions } from './CreateTable/CreateTableInstructions' import { NamespaceWithTables } from './NamespaceWithTables' @@ -31,17 +30,9 @@ import { ScaffoldSectionTitle, } from '@/components/layouts/Scaffold' import AlertError from '@/components/ui/AlertError' -import { InlineLink } from '@/components/ui/InlineLink' -import { - DatabaseExtension, - useDatabaseExtensionsQuery, -} from '@/data/database-extensions/database-extensions-query' -import { useReplicationPipelineStatusQuery } from '@/data/replication/pipeline-status-query' -import { useStartPipelineMutation } from '@/data/replication/start-pipeline-mutation' +import { useDatabaseExtensionsQuery } from '@/data/database-extensions/database-extensions-query' import { useIcebergNamespacesQuery } from '@/data/storage/iceberg-namespaces-query' -import { useIcebergWrapperCreateMutation } from '@/data/storage/iceberg-wrapper-create-mutation' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' -import { DOCS_URL } from '@/lib/constants' export const AnalyticBucketDetails = () => { const router = useRouter() @@ -66,30 +57,14 @@ export const AnalyticBucketDetails = () => { const [pollIntervalNamespaces, setPollIntervalNamespaces] = useState(0) const [pollIntervalNamespaceTables, setPollIntervalNamespaceTables] = useState(0) - const { mutateAsync: startPipeline, isPending: isStartingPipeline } = useStartPipelineMutation() - const { publication, - pipeline, icebergWrapper: wrapperInstance, isLoadingWrapperInstance, } = useAnalyticsBucketAssociatedEntities({ projectRef, bucketId: bucket?.name, }) - const { data, isSuccess: isSuccessPipelineStatus } = useReplicationPipelineStatusQuery( - { projectRef, pipelineId: pipeline?.id }, - { - refetchInterval: (query) => { - const data = query.state.data - if (data?.status.name !== 'started') return 4000 - else return false - }, - } - ) - const pipelineStatus = data?.status.name - const isPipelineRunning = pipelineStatus === 'started' - const isPipelineStopped = ['failed', 'stopped'].includes(pipelineStatus ?? '') const wrapperValues = convertKVStringArrayToJson(wrapperInstance?.server_options ?? []) const integration = INTEGRATIONS.find((i) => i.id === 'iceberg_wrapper' && i.type === 'wrapper') @@ -224,69 +199,20 @@ export const AnalyticBucketDetails = () => { ) : null} ) : ( - <> - {!!pipeline && !!isSuccessPipelineStatus && !isPipelineRunning && ( - - - {isPipelineStopped && ( - - )} - - } - > - {!isPipelineStopped && ( - - )} - - )} -
- {namespaces.map(({ namespace, schema, tables }) => ( - - ))} -
- +
+ {namespaces.map(({ namespace, schema, tables }) => ( + + ))} +
)} @@ -329,125 +255,3 @@ export const AnalyticBucketDetails = () => { ) } - -const ExtensionNotInstalled = ({ - bucketName, - projectRef, - wrapperMeta, - wrappersExtension, -}: { - bucketName?: string - projectRef: string - wrapperMeta: WrapperMeta - wrappersExtension: DatabaseExtension -}) => { - const databaseNeedsUpgrading = - (wrappersExtension?.default_version ?? '') < (wrapperMeta?.minimumExtensionVersion ?? '') - - return ( - <> - - -

- The Wrappers extension is required in order to query analytics tables.{' '} - {databaseNeedsUpgrading && - 'Please first upgrade your database and then install the extension.'}{' '} - - Learn more - -

- -
-
- - - ) -} - -const ExtensionNeedsUpgrade = ({ - bucketName, - projectRef, - wrapperMeta, - wrappersExtension, -}: { - bucketName?: string - projectRef: string - wrapperMeta: WrapperMeta - wrappersExtension: DatabaseExtension -}) => { - // [Joshen] Default version is what's on the DB, so if the installed version is already the default version - // but still doesnt meet the minimum extension version, then DB upgrade is required - const databaseNeedsUpgrading = - wrappersExtension?.installed_version === wrappersExtension?.default_version - - return ( - <> - - -

- The {wrapperMeta.label} wrapper requires a minimum extension version of{' '} - {wrapperMeta.minimumExtensionVersion}. You have version{' '} - {wrappersExtension?.installed_version} installed. Please{' '} - {databaseNeedsUpgrading && 'first upgrade your database, and then '}update the extension - by disabling and enabling the Wrappers extension. -

-

- Before reinstalling the wrapper extension, you must first remove all existing wrappers. - Afterward, you can recreate the wrappers. -

- -
-
- - - ) -} - -const WrapperMissing = ({ bucketName }: { bucketName?: string }) => { - const { mutateAsync: createIcebergWrapper, isPending: isCreatingIcebergWrapper } = - useIcebergWrapperCreateMutation() - - const onSetupWrapper = async () => { - if (!bucketName) return console.error('Bucket name is required') - await createIcebergWrapper({ bucketName }) - } - - return ( - <> - - -

The Iceberg Wrapper integration is required in order to query analytics tables.

- -
-
- - - ) -} diff --git a/apps/studio/data/vault/vault-secret-update-mutation.ts b/apps/studio/data/vault/vault-secret-update-mutation.ts index 942f722fdf8..f53237d998e 100644 --- a/apps/studio/data/vault/vault-secret-update-mutation.ts +++ b/apps/studio/data/vault/vault-secret-update-mutation.ts @@ -10,6 +10,7 @@ export type VaultSecretUpdateVariables = { projectRef: string connectionString?: string | null id: string + skipClearCache?: boolean } & Partial export async function updateVaultSecret({ @@ -40,9 +41,15 @@ export const useVaultSecretUpdateMutation = ({ return useMutation({ mutationFn: (vars) => updateVaultSecret(vars), async onSuccess(data, variables, context) { - const { id, projectRef } = variables + const { id, projectRef, skipClearCache = false } = variables await Promise.all([ - queryClient.removeQueries({ queryKey: vaultSecretsKeys.getDecryptedValue(projectRef, id) }), + !skipClearCache + ? queryClient.removeQueries({ + queryKey: vaultSecretsKeys.getDecryptedValue(projectRef, id), + }) + : queryClient.invalidateQueries({ + queryKey: vaultSecretsKeys.getDecryptedValue(projectRef, id), + }), queryClient.invalidateQueries({ queryKey: vaultSecretsKeys.list(projectRef) }), ]) await onSuccess?.(data, variables, context)