mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
## 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 -->
431 lines
13 KiB
TypeScript
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>
|
|
)
|
|
}
|