Files
supabase/apps/studio/components/interfaces/Integrations/Warehouse/WarehouseConnectSection.tsx
Danny White 3e2d54eccb feat(studio): add Warehouse table management and disable (#50195)
## What kind of change does this PR introduce?

Feature and UI polish.

## What is the current behavior?

Warehouse setup uses a schema accordion for table selection. Once
Warehouse is enabled, users cannot remove replicated tables or disable
Warehouse from Studio.

## What is the new behavior?

- Replaces the schema accordion with one grouped, searchable table
selector.
- Still allows for **Select all** and **Clear** actions for each schema.
- Starts first-time setup with no tables selected and preselects current
replicated tables when editing.
	- Adds support for removing previously replicated tables.
- Adds a confirmed **Disable Warehouse** action.
- Tracks successful Warehouse enable and disable actions.

Disabling Warehouse removes its replication pipeline, publication,
catalogue access, and foreign tables. Copied data remains in DuckLake
storage until the user deletes it. Re-enabling a table rebuilds its data
rather than reusing the retained copy.

| Before | After |
| --- | --- |
| <img width="1024" height="759" alt="Integrations Test US East 1 testdw
Supabase"
src="https://github.com/user-attachments/assets/bded025b-1d45-41dc-8a35-9159baf8f9b7"
/> | <img width="1024" height="759" alt="Integrations test Teamer
Supabase"
src="https://github.com/user-attachments/assets/69026d94-98a0-4878-ab58-2e9697296d93"
/> |
| <img width="1280" height="1323" alt="Integrations Test testdw
Supabase"
src="https://github.com/user-attachments/assets/3f71e754-1a87-4d58-a7b9-dd39d3e0ac5a"
/> | <img width="1280" height="1323" alt="Integrations Regular AWS
Teamer Supabase"
src="https://github.com/user-attachments/assets/758ed48e-9ed6-45d3-ae94-e171147a21d5"
/> |
| _Feature did not exist_ | <img width="1024" height="759"
alt="Integrations Regular AWS Teamer Supabase"
src="https://github.com/user-attachments/assets/c977ac57-8b0c-4482-882b-69ad7602b5df"
/> |

## Additional context

Platform support for updating and disabling Warehouse was added in
[supabase/platform#38190](https://github.com/supabase/platform/pull/38190).

### To test

1. Open `/project/{ref}/integrations/warehouse/overview` before setup.
2. Confirm **Tables to replicate** starts at zero and **Enable
Warehouse** is disabled until a table is selected.
3. Confirm each schema's **Select all** and **Clear** actions update
every table in that schema.
4. Enable Warehouse with a partial selection and wait for setup to
complete.
5. Edit the selection, add and remove replicated tables, then confirm
the saved selection is reflected in the publication.
6. Disable Warehouse, confirm the retention warning, and verify the
integration returns to its initial state.
7. Re-enable Warehouse and confirm selected tables are rebuilt.
8. Trigger a replication pipeline limit error and confirm the inline
guidance links to Database Replication.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
  - Added the ability to disable Warehouse from the setup panel.
  - Warehouse setup now starts with no table selections.
- Editing a setup preselects replicated tables and supports updating
selections, including removing tables.
- Added searchable schema and table selection with screen-reader count
announcements.
  - Added telemetry tracking for initial Warehouse enablement.

- **Bug Fixes**
- Warehouse disable failures now show an error while keeping the
confirmation dialog open for retry.
  - Configuration updates now refresh related data automatically.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-17 14:17:00 +10:00

431 lines
13 KiB
TypeScript

import { useParams } from 'common'
import { Eye, EyeOff, KeyRound, Loader2 } from 'lucide-react'
import Link from 'next/link'
import { useRef, useState } from 'react'
import { toast } from 'sonner'
import {
Button,
Card,
CardContent,
cn,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
Separator,
Switch,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import { CodeBlock } from 'ui-patterns/CodeBlock'
import { Input } from 'ui-patterns/DataInputs/Input'
import { FormLayout } from 'ui-patterns/form/Layout/FormLayout'
import {
PageSection,
PageSectionContent,
PageSectionDescription,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { ConnectSheetStep } from '../../ConnectSheet/ConnectSheetStep'
import { EnvRow } from '../../ConnectSheet/content/server/common/EnvRow'
import { CopyPromptButton } from '../../ConnectSheet/CopyPromptAdmonition'
import { useConnectSheetParams } from '../../ConnectSheet/useConnectSheetParams'
import type { WarehouseCatalogCredentials } from './Warehouse.utils'
import { AlertError } from '@/components/ui/AlertError'
import CopyButton from '@/components/ui/CopyButton'
import { useUpdateWarehouseCatalogMutation } from '@/data/warehouse/warehouse-catalog-mutation'
import { useWarehouseCatalogQuery } from '@/data/warehouse/warehouse-catalog-query'
import {
DUCKLAKE_METADATA_PASSWORD_ENV_VAR,
DUCKLAKE_S3_SECRET_ENV_VAR,
getDuckLakeSetupScript,
getWarehouseFlightSqlConnectionString,
getWarehouseFlightSqlEndpoint,
getWarehouseUsqlCommand,
parseWarehouseCatalogUrl,
} from '@/lib/warehouse'
/**
* Only engines we can produce real configuration for. DuckLake exposes a Postgres catalog over S3
* rather than an Iceberg REST catalog, so Trino, Spark and PyIceberg need API support first.
*/
const QUERY_ENGINES = [
{ value: 'flightsql', label: 'FlightSQL' },
{ value: 'duckdb', label: 'DuckDB' },
] as const
type QueryEngine = (typeof QUERY_ENGINES)[number]['value']
function FieldRow({
id,
label,
children,
}: {
id?: string
label: React.ReactNode
children: React.ReactNode
}) {
return (
<FormLayout id={id} layout="horizontal" label={label}>
{children}
</FormLayout>
)
}
const FlightSqlContent = ({ projectRef }: { projectRef: string }) => (
<CardContent className="space-y-4">
<FieldRow id="warehouse-flightsql-endpoint" label="Endpoint">
<Input
id="warehouse-flightsql-endpoint"
readOnly
copy
className="font-mono"
value={getWarehouseFlightSqlEndpoint(projectRef)}
/>
</FieldRow>
<FieldRow id="warehouse-flightsql-connection-string" label="Connection string">
<Input
id="warehouse-flightsql-connection-string"
readOnly
copy
className="font-mono"
value={getWarehouseFlightSqlConnectionString(projectRef)}
/>
</FieldRow>
<FieldRow id="warehouse-flightsql-user" label="User">
<Input id="warehouse-flightsql-user" readOnly copy className="font-mono" value="postgres" />
</FieldRow>
<FieldRow label="Password">
<div className="flex justify-end">
<Button asChild variant="default" size="tiny" icon={<KeyRound size={14} />}>
<Link href={`/project/${projectRef}/settings/database`}>Reset database password</Link>
</Button>
</div>
</FieldRow>
<FieldRow label="Command line">
<CodeBlock
className="[&_code]:text-foreground"
language="bash"
hideLineNumbers
wrapLongLines
value={getWarehouseUsqlCommand(projectRef)}
/>
</FieldRow>
</CardContent>
)
const DuckLakeSecretRow = ({ name, value }: { name: string; value: string }) => {
const [isRevealed, setIsRevealed] = useState(false)
return (
<EnvRow name={name} value={isRevealed ? value : '•'.repeat(16)}>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="default"
size="tiny"
className="px-1.5"
aria-label={`${isRevealed ? 'Hide' : 'Reveal'} ${name}`}
icon={isRevealed ? <EyeOff strokeWidth={2} /> : <Eye strokeWidth={2} />}
onClick={() => setIsRevealed((value) => !value)}
/>
</TooltipTrigger>
<TooltipContent side="bottom">
{isRevealed ? 'Hide environment variable' : 'Reveal environment variable'}
</TooltipContent>
</Tooltip>
<CopyButton variant="default" size="tiny" iconOnly aria-label={`Copy ${name}`} text={value} />
</EnvRow>
)
}
/**
* The DuckDB setup script inlines everything except the two passwords, which it reads via
* `getenv()` -- so those are the only credential values surfaced as their own rows here.
*/
const DuckLakeEnvironmentVariables = ({
credentials,
password,
}: {
credentials: WarehouseCatalogCredentials
password: string
}) => {
const environmentVariables = [
`${DUCKLAKE_S3_SECRET_ENV_VAR}=${credentials.s3_secret_access_key}`,
`${DUCKLAKE_METADATA_PASSWORD_ENV_VAR}=${password}`,
].join('\n')
return (
<div className="overflow-hidden rounded-lg border bg-surface-75" data-connect-prompt-ignore>
<div className="flex items-center justify-between border-b bg-surface-100 py-2 pl-4 pr-2">
<span className="font-mono text-xs text-foreground-light">.env</span>
<CopyButton
variant="default"
size="tiny"
copyLabel="Copy all"
aria-label="Copy all DuckLake environment variables"
text={environmentVariables}
/>
</div>
<div className="divide-y">
<DuckLakeSecretRow
name={DUCKLAKE_S3_SECRET_ENV_VAR}
value={credentials.s3_secret_access_key}
/>
<DuckLakeSecretRow name={DUCKLAKE_METADATA_PASSWORD_ENV_VAR} value={password} />
</div>
</div>
)
}
const DuckLakeSetup = ({
credentials,
variant,
}: {
credentials: WarehouseCatalogCredentials
variant: 'default' | 'sheet'
}) => {
const connection = parseWarehouseCatalogUrl(credentials.catalog_url)
const stepsContainerRef = useRef<HTMLDivElement | null>(null)
if (connection === null) {
const errorContent = (
<>
<Admonition
type="warning"
title="Could not read the catalog connection details"
description="Copy the catalog URL and configure the DuckLake secrets manually."
/>
<FieldRow id="warehouse-catalog-url" label="Catalog URL">
<Input
id="warehouse-catalog-url"
readOnly
copy
reveal
className="font-mono"
value={credentials.catalog_url}
/>
</FieldRow>
</>
)
return variant === 'sheet' ? (
<div className="space-y-4 border-t bg-muted/50 p-8">{errorContent}</div>
) : (
<CardContent className="space-y-4">{errorContent}</CardContent>
)
}
const script = (
<CodeBlock
className="[&_code]:text-foreground"
language="sql"
hideLineNumbers
value={getDuckLakeSetupScript({ credentials, connection })}
/>
)
if (variant === 'default') {
return (
<CardContent className="space-y-4">
<DuckLakeEnvironmentVariables credentials={credentials} password={connection.password} />
{script}
</CardContent>
)
}
return (
<div className="border-t bg-muted/50 p-8">
<div className="mb-6 flex items-center justify-between gap-4">
<h3>Follow these steps</h3>
<CopyPromptButton stepsContainerRef={stepsContainerRef} />
</div>
<div ref={stepsContainerRef}>
<ConnectSheetStep
number={1}
title="Set environment variables"
description="Add these credentials to your environment before running the SQL."
>
<DuckLakeEnvironmentVariables credentials={credentials} password={connection.password} />
</ConnectSheetStep>
<ConnectSheetStep
number={2}
title="Attach Warehouse"
description="Run this script in DuckDB to configure the secrets and attach Warehouse."
>
{script}
</ConnectSheetStep>
</div>
</div>
)
}
const CatalogAccessToggle = ({
projectRef,
isEnabled,
}: {
projectRef: string
isEnabled: boolean
}) => {
const catalogMutation = useUpdateWarehouseCatalogMutation({
onSuccess: (catalog) =>
toast.success(
catalog?.enabled ? 'DuckDB catalog access enabled' : 'DuckDB catalog access disabled'
),
})
return (
<CardContent>
<FormLayout
layout="flex-row-reverse"
label="Enable DuckDB catalog access"
description="Creates the credentials DuckDB needs to attach Warehouse. Not required for FlightSQL."
>
<div className="flex items-center justify-end gap-2">
<span
role="status"
aria-label={catalogMutation.isPending ? 'Updating DuckDB catalog access' : undefined}
className="sr-only"
>
{catalogMutation.isPending ? 'Updating DuckDB catalog access' : ''}
</span>
{catalogMutation.isPending && (
<Loader2
size={16}
className="animate-spin text-foreground-muted motion-reduce:animate-none"
aria-hidden
/>
)}
<Switch
aria-label="Enable DuckDB catalog access"
aria-busy={catalogMutation.isPending}
checked={isEnabled}
disabled={catalogMutation.isPending}
onCheckedChange={(enabled) => catalogMutation.mutate({ projectRef, body: { enabled } })}
/>
</div>
</FormLayout>
</CardContent>
)
}
interface WarehouseConnectionCardProps {
variant?: 'default' | 'sheet'
}
export const WarehouseConnectionCard = ({ variant = 'default' }: WarehouseConnectionCardProps) => {
const { ref: projectRef } = useParams()
const { params, setConnectParams } = useConnectSheetParams()
const engine: QueryEngine = params.warehouseQueryEngine === 'duckdb' ? 'duckdb' : 'flightsql'
const {
data: catalog,
isPending: isCatalogPending,
isError: isCatalogError,
error: catalogError,
} = useWarehouseCatalogQuery({ projectRef }, { enabled: engine === 'duckdb' })
if (!projectRef) return null
let catalogStatus = ''
if (engine === 'duckdb' && isCatalogPending) catalogStatus = 'Loading DuckDB catalog access'
if (engine === 'duckdb' && !isCatalogPending && !isCatalogError) {
catalogStatus = 'DuckDB catalog access loaded'
}
const isSheet = variant === 'sheet'
const connectionCard = (
<Card
className={cn(
isSheet &&
'relative space-y-4 rounded-none border-0 bg-transparent shadow-none [&>div]:border-0 [&>div]:p-0'
)}
>
<CardContent className="border-none">
<FieldRow id="warehouse-query-engine" label="Query engine">
<Select
value={engine}
onValueChange={(value) =>
setConnectParams({ warehouseQueryEngine: value as QueryEngine })
}
>
<SelectTrigger id="warehouse-query-engine" className="ml-auto w-48">
<SelectValue />
</SelectTrigger>
<SelectContent align="end">
{QUERY_ENGINES.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</FieldRow>
</CardContent>
{!isSheet && <Separator />}
{engine === 'flightsql' && <FlightSqlContent projectRef={projectRef} />}
{engine === 'duckdb' && (
<>
{isCatalogPending && (
<CardContent>
<GenericSkeletonLoader />
</CardContent>
)}
{isCatalogError && (
<CardContent>
<AlertError subject="Failed to load DuckLake catalog access" error={catalogError} />
</CardContent>
)}
{catalog && <CatalogAccessToggle projectRef={projectRef} isEnabled={catalog.enabled} />}
{!isSheet && catalog?.enabled && catalog.credentials && (
<DuckLakeSetup credentials={catalog.credentials} variant="default" />
)}
</>
)}
<span className="sr-only" role="status" aria-live="polite">
{catalogStatus}
</span>
</Card>
)
if (!isSheet) return connectionCard
return (
<div>
<div className="p-8">{connectionCard}</div>
{engine === 'duckdb' && catalog?.enabled && catalog.credentials && (
<DuckLakeSetup credentials={catalog.credentials} variant="sheet" />
)}
</div>
)
}
export const WarehouseConnectSection = () => {
return (
<PageSection className="pt-5!">
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Connect</PageSectionTitle>
<PageSectionDescription>
Point an analytical tool at Warehouse without querying your primary database.
</PageSectionDescription>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<WarehouseConnectionCard />
</PageSectionContent>
</PageSection>
)
}