Add user flow for iceberg wrapper if api keys are rotated (#47336)

## 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:
<img width="1110" height="320" alt="Screenshot 2026-06-26 at 17 33 11"
src="https://github.com/user-attachments/assets/28ea4ce6-5b81-4217-9952-880acb02f2bd"
/>

We'll subsequently also float this issue up in the Analytics Bucket UI
(which is linked from the contextual error above)
<img width="1114" height="466" alt="Screenshot 2026-06-26 at 17 31 52"
src="https://github.com/user-attachments/assets/8d112e5b-6ecc-458b-b4dc-7e7647da3fb2"
/>

And users can then choose to use another API key as the catalog token
<img width="585" height="246" alt="Screenshot 2026-06-26 at 17 31 56"
src="https://github.com/user-attachments/assets/3d9689a5-b18d-4f07-a5a5-d882e41c5958"
/>

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

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Joshen Lim authored and GitHub committed 2026-06-29 17:43:38 +08:00
1 parent 8192d97008
commit 3d10f2cab9
7 files changed
+398 -217

No files matched your search

@@ -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 <FilterError removeAllFilters={removeAllFilters} />
} else if (isInvalidOrderingOperatorError) {
return <InvalidOrderingOperatorError error={error} />
} else if (isIcebergUnauthorizedError) {
return <IcebergUnauthorizedError error={error} />
}
return <GeneralError error={error} />
@@ -165,6 +174,30 @@ const InvalidOrderingOperatorError = ({ error }: { error: ResponseError }) => {
)
}
const IcebergUnauthorizedError = ({ error }: { error: ResponseError }) => {
const { ref } = useParams()
return (
<Admonition
type="warning"
className="pointer-events-auto"
title="Failed to retrieve rows from Iceberg foreign table"
>
<p className="text-balance">
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{' '}
<InlineLink href={`/project/${ref}/storage/analytics`}>Iceberg Bucket</InlineLink>.
Alternatively, you can also verify the token value in your{' '}
<InlineLink href={`/project/${ref}/integrations/iceberg_wrapper/wrappers`}>
wrapper's settings
</InlineLink>{' '}
or in <InlineLink href={`/project/${ref}/integrations/vault/overview`}>Vault</InlineLink>.
</p>
<ExpandError error={error} />
</Admonition>
)
}
const GeneralError = ({ error }: { error: ResponseError }) => {
const { filters } = useTableFilter()
@@ -183,3 +216,19 @@ const GeneralError = ({ error }: { error: ResponseError }) => {
</AlertError>
)
}
const ExpandError = ({ error }: { error: ResponseError }) => {
return (
<Collapsible>
<CollapsibleTrigger className="mt-2 group font-normal p-0 [&[data-state=open]>div>svg]:-rotate-180!">
<div className="flex items-center gap-x-2 w-full cursor-pointer">
<span className="font-mono uppercase tracking-tight">View error</span>
<ChevronDown className="transition-transform" size={14} />
</div>
</CollapsibleTrigger>
<CollapsibleContent className="mt-1">
<code className="text-code-inline">{error.message}</code>
</CollapsibleContent>
</Collapsible>
)
}
@@ -19,7 +19,7 @@ export const LABELS: Record<string, string> = {
export const DESCRIPTIONS: Record<string, string> = {
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: '',
@@ -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 (
<>
<ScaffoldSection isFullWidth>
<Admonition type="warning" title="Missing required extension">
<p>
The Wrappers extension is required in order to query analytics tables.{' '}
{databaseNeedsUpgrading &&
'Please first upgrade your database and then install the extension.'}{' '}
<InlineLink
href={`${DOCS_URL}/guides/database/extensions/wrappers/iceberg`}
target="_blank"
rel="noreferrer"
className="text-foreground-lighter hover:text-foreground transition-colors"
>
Learn more
</InlineLink>
</p>
<Button variant="default" asChild className="mt-2" onClick={() => {}}>
<Link
href={
databaseNeedsUpgrading
? `/project/${projectRef}/settings/infrastructure`
: `/project/${projectRef}/database/extensions?filter=wrappers`
}
>
{databaseNeedsUpgrading ? 'Upgrade database' : 'Install extension'}
</Link>
</Button>
</Admonition>
</ScaffoldSection>
<SimpleConfigurationDetails bucketName={bucketName} />
</>
)
}
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 (
<>
<ScaffoldSection isFullWidth>
<Admonition type="warning" title="Outdated extension version">
<p>
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.
</p>
<p>
Before reinstalling the wrapper extension, you must first remove all existing wrappers.
Afterward, you can recreate the wrappers.
</p>
<Button asChild variant="default">
<Link
href={
databaseNeedsUpgrading
? `/project/${projectRef}/settings/infrastructure`
: `/project/${projectRef}/database/extensions?filter=wrappers`
}
>
{databaseNeedsUpgrading ? 'Upgrade database' : 'Extensions'}
</Link>
</Button>
</Admonition>
</ScaffoldSection>
<SimpleConfigurationDetails bucketName={bucketName} />
</>
)
}
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 (
<>
<ScaffoldSection isFullWidth>
<Admonition type="warning" title="Missing integration">
<p>The Iceberg Wrapper integration is required in order to query analytics tables.</p>
<Button variant="default" loading={isCreatingIcebergWrapper} onClick={onSetupWrapper}>
Install wrapper
</Button>
</Admonition>
</ScaffoldSection>
<SimpleConfigurationDetails bucketName={bucketName} />
</>
)
}
@@ -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
/>
</ScaffoldHeader>
{isSuccessApiKeys && isSuccessVaultDecrypt && !isTokenValid && (
<Admonition type="warning" title="Catalog token is no longer valid" className="mb-4">
<p>
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.
</p>
{canReadAPIKeys && <UpdateCatalogTokenDialog vaultTokenId={wrapperValues?.vault_token} />}
</Admonition>
)}
<Card>
{wrapperMeta.server.options
.filter((option) => !option.hidden && wrapperValues[option.name])
@@ -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<string>()
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 (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="default" className="mt-1">
Update catalog token
</Button>
</DialogTrigger>
<DialogContent aria-describedby={undefined}>
<DialogHeader>
<DialogTitle>Update Iceberg wrapper catalog token</DialogTitle>
</DialogHeader>
<DialogSectionSeparator />
{allSecretKeys?.length === 0 && (
<Admonition
type="default"
title="Project has no API secret keys"
className="rounded-none border-x-0 border-t-0"
>
<p>
Create an API secret key from your{' '}
<InlineLink href={`/project/${ref}/settings/api-keys`}>project's settings</InlineLink>{' '}
first
</p>
</Admonition>
)}
<DialogSection>
<FormItemLayout
isReactForm={false}
label="Select a secret key to use as your catalog token"
>
{isPending ? (
<ShimmeringLoader className="py-4" />
) : (
<Select value={selectedKey} onValueChange={setSelectedKey}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{apiKeysData?.allSecretKeys.map((x) => (
<SelectItem key={x.id} value={x.api_key}>
{x.prefix} •••
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
)}
</FormItemLayout>
</DialogSection>
<DialogFooter>
<Button variant="default" disabled={isUpdating} onClick={() => setOpen(false)}>
Cancel
</Button>
<Button
variant="primary"
disabled={!selectedKey}
loading={isUpdating}
onClick={() => onSubmit()}
>
Update token
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
@@ -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 && (
<Admonition
type="note"
layout="horizontal"
className="[&>div]:pl-10 [&>div]:translate-y-[-3px]"
childProps={{ title: { className: 'block capitalize-sentence' } }}
showIcon={isPipelineStopped}
title={
isPipelineStopped
? `Replication on the bucket has ${pipelineStatus}`
: `${pipelineStatus} replication on the bucket...`
}
description={
isPipelineStopped
? 'Data changes from Postgres tables is currently not streaming to their corresponding analytics bucket table'
: 'Data changes from Postgres tables will resume streaming once pipeline has started'
}
actions={
<div className="flex items-center gap-x-2">
<Button asChild variant="default">
<Link
href={`/project/${projectRef}/database/replication/${pipeline.replicator_id}`}
>
View pipeline
</Link>
</Button>
{isPipelineStopped && (
<Button
variant="default"
loading={isStartingPipeline}
onClick={async () => {
if (projectRef) {
await startPipeline({ projectRef, pipelineId: pipeline.id })
}
}}
>
Restart
</Button>
)}
</div>
}
>
{!isPipelineStopped && (
<Loader2 size={18} className="absolute top-1.5 left-[3px] animate-spin" />
)}
</Admonition>
)}
<div className="flex flex-col gap-y-10">
{namespaces.map(({ namespace, schema, tables }) => (
<NamespaceWithTables
key={namespace}
namespace={namespace}
sourceType="direct"
schema={schema}
tables={tables as any}
wrapperValues={wrapperValues}
pollIntervalNamespaceTables={pollIntervalNamespaceTables}
setPollIntervalNamespaceTables={setPollIntervalNamespaceTables}
/>
))}
</div>
</>
<div className="flex flex-col gap-y-10">
{namespaces.map(({ namespace, schema, tables }) => (
<NamespaceWithTables
key={namespace}
namespace={namespace}
sourceType="direct"
schema={schema}
tables={tables as any}
wrapperValues={wrapperValues}
pollIntervalNamespaceTables={pollIntervalNamespaceTables}
setPollIntervalNamespaceTables={setPollIntervalNamespaceTables}
/>
))}
</div>
)}
</ScaffoldSection>
@@ -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 (
<>
<ScaffoldSection isFullWidth>
<Admonition type="warning" title="Missing required extension">
<p>
The Wrappers extension is required in order to query analytics tables.{' '}
{databaseNeedsUpgrading &&
'Please first upgrade your database and then install the extension.'}{' '}
<InlineLink
href={`${DOCS_URL}/guides/database/extensions/wrappers/iceberg`}
target="_blank"
rel="noreferrer"
className="text-foreground-lighter hover:text-foreground transition-colors"
>
Learn more
</InlineLink>
</p>
<Button variant="default" asChild className="mt-2" onClick={() => {}}>
<Link
href={
databaseNeedsUpgrading
? `/project/${projectRef}/settings/infrastructure`
: `/project/${projectRef}/database/extensions?filter=wrappers`
}
>
{databaseNeedsUpgrading ? 'Upgrade database' : 'Install extension'}
</Link>
</Button>
</Admonition>
</ScaffoldSection>
<SimpleConfigurationDetails bucketName={bucketName} />
</>
)
}
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 (
<>
<ScaffoldSection isFullWidth>
<Admonition type="warning" title="Outdated extension version">
<p>
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.
</p>
<p>
Before reinstalling the wrapper extension, you must first remove all existing wrappers.
Afterward, you can recreate the wrappers.
</p>
<Button asChild variant="default">
<Link
href={
databaseNeedsUpgrading
? `/project/${projectRef}/settings/infrastructure`
: `/project/${projectRef}/database/extensions?filter=wrappers`
}
>
{databaseNeedsUpgrading ? 'Upgrade database' : 'Extensions'}
</Link>
</Button>
</Admonition>
</ScaffoldSection>
<SimpleConfigurationDetails bucketName={bucketName} />
</>
)
}
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 (
<>
<ScaffoldSection isFullWidth>
<Admonition type="warning" title="Missing integration">
<p>The Iceberg Wrapper integration is required in order to query analytics tables.</p>
<Button variant="default" loading={isCreatingIcebergWrapper} onClick={onSetupWrapper}>
Install wrapper
</Button>
</Admonition>
</ScaffoldSection>
<SimpleConfigurationDetails bucketName={bucketName} />
</>
)
}
@@ -10,6 +10,7 @@ export type VaultSecretUpdateVariables = {
projectRef: string
connectionString?: string | null
id: string
skipClearCache?: boolean
} & Partial<VaultSecret>
export async function updateVaultSecret({
@@ -40,9 +41,15 @@ export const useVaultSecretUpdateMutation = ({
return useMutation<VaultSecretUpdateData, ResponseError, VaultSecretUpdateVariables>({
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)