mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
## What kind of change does this PR introduce? Feature and information architecture change. Builds on #50246. ## What is the current behaviour? Warehouse setup, progress, errors, table selection, and connection details all live in the transient Connect sheet. Closing the sheet hides the current replication state, and the integration is absent from the Integrations page. ## What is the new behaviour? Warehouse now has a persistent Overview page at `/project/{ref}/integrations/warehouse/overview`: - Before setup, the existing schema and table picker enables Warehouse. - During setup, the page shows the current phase and per-table backfill state where available. - Setup and status failures remain visible on the page with retry actions where possible. - Once complete, the page shows Status, Tables, then Connect. - The Connect sheet becomes read-only. Before Warehouse is ready, it links directly to the Overview page for setup, progress, or recovery. | Before | After | | --- | --- | | <img width="1200" height="907" alt="Chives Pantry Supabase" src="https://github.com/user-attachments/assets/82dc7fb0-3859-499e-96ab-56f70a5c7325" /> | <img width="1200" height="907" alt="Chives Pantry Supabase" src="https://github.com/user-attachments/assets/7428e301-27f8-48fe-91e0-6880b132920d" /> | | <img width="1200" height="907" alt="Chives Pantry Supabase" src="https://github.com/user-attachments/assets/82dc7fb0-3859-499e-96ab-56f70a5c7325" /> | <img width="1200" height="907" alt="54709" src="https://github.com/user-attachments/assets/0c589e5b-e13c-4554-a6ee-6730d0e95c07" /> | | <img width="1200" height="907" alt="ETL BigTable ETL Team Supabase" src="https://github.com/user-attachments/assets/18ec9f6c-3724-453f-bbbb-c7149758246e" /> | <img width="1200" height="907" alt="Regular AWS Teamer Supabase" src="https://github.com/user-attachments/assets/3819c9fe-e56d-4cec-988d-5e724f8d990a" /> | ## To test Use a project whose organisation is included in the Warehouse allow-list. 1. Open `/project/{ref}/integrations`, filter by **Data platform**, and open Warehouse. 2. Before setup, confirm the existing schema and table picker appears and starts with no tables selected. 3. Start setup and confirm the Status section polls through setup and table backfill phases. 4. Confirm setup failures remain visible and expose Retry when the API returns affected tables. 5. After setup, confirm the section order is Status, Tables, Connect. 6. Confirm existing replicated tables are selected and locked, while additional tables can be added. 7. Open `/project/{ref}?showConnect=true&connectTab=warehouse` and confirm it links to the Overview page before setup, during setup, and after a setup failure. 8. Once setup is complete, confirm the Connect sheet shows the FlightSQL and DuckDB connection controls from #50246. 9. Repeat the Overview checks with **One-Click Integrations** turned off. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added Supabase Warehouse to the integrations catalog, with overview documentation and availability-aware display. * Added Warehouse setup and management flows, including schema and table selection, replication progress, status details, connection options, and retry actions. * Added DuckDB and FlightSQL engine selection with Connect sheet URL and preference synchronization. * Added table replication status, lag, timestamps, and size information. * **Bug Fixes** * Warehouse setup status requests no longer retry automatically after failures. * Improved recovery messaging and retry behavior for setup and connection errors. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
319 lines
13 KiB
TypeScript
319 lines
13 KiB
TypeScript
import { useParams } from 'common'
|
|
import { ChevronRight } from 'lucide-react'
|
|
import { useMemo, useState } from 'react'
|
|
import {
|
|
Button,
|
|
Card,
|
|
CardContent,
|
|
CardFooter,
|
|
Checkbox,
|
|
Collapsible,
|
|
CollapsibleContent,
|
|
CollapsibleTrigger,
|
|
} from 'ui'
|
|
import {
|
|
PageSection,
|
|
PageSectionContent,
|
|
PageSectionDescription,
|
|
PageSectionMeta,
|
|
PageSectionSummary,
|
|
PageSectionTitle,
|
|
} from 'ui-patterns/PageSection'
|
|
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
|
|
|
import {
|
|
buildSelectionFromPublicationTables,
|
|
buildWarehouseSetupTargets,
|
|
getSchemaCheckedState,
|
|
getSchemaTableKey,
|
|
getSelectedTableCount,
|
|
hasSelectionChanged,
|
|
isSelectableWarehouseSchema,
|
|
type SchemaTableSelection,
|
|
type SchemaWithTables,
|
|
type WarehouseSetupTarget,
|
|
} from './Warehouse.utils'
|
|
import { AlertError } from '@/components/ui/AlertError'
|
|
import { useSchemasQuery } from '@/data/database/schemas-query'
|
|
import { useReplicationPublicationQuery } from '@/data/replication/publication-query'
|
|
import { useReplicationSourcesQuery } from '@/data/replication/sources-query'
|
|
import { useTablesQuery } from '@/data/tables/tables-query'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
import { WAREHOUSE_PUBLICATION_NAME } from '@/lib/warehouse'
|
|
|
|
export interface WarehouseSchemaTablePickerProps {
|
|
onSubmit: (targets: WarehouseSetupTarget[]) => void
|
|
isSubmitting: boolean
|
|
isEditing?: boolean
|
|
error?: { message: string } | null
|
|
}
|
|
|
|
export const WarehouseSchemaTablePicker = ({
|
|
onSubmit,
|
|
isSubmitting,
|
|
isEditing = false,
|
|
error,
|
|
}: WarehouseSchemaTablePickerProps) => {
|
|
const { ref: projectRef } = useParams()
|
|
const { data: project } = useSelectedProjectQuery()
|
|
|
|
// `null` until the user touches a checkbox, so the selection seeded from the existing
|
|
// publication can arrive asynchronously without an effect syncing it into state.
|
|
const [selectionOverride, setSelectionOverride] = useState<SchemaTableSelection | null>(null)
|
|
const [expandedOverrides, setExpandedOverrides] = useState<Record<string, boolean>>({})
|
|
|
|
const {
|
|
data: schemas,
|
|
isPending: isSchemasPending,
|
|
isError: isSchemasError,
|
|
error: schemasError,
|
|
} = useSchemasQuery({ projectRef, connectionString: project?.connectionString })
|
|
|
|
const {
|
|
data: tables,
|
|
isPending: isTablesPending,
|
|
isError: isTablesError,
|
|
error: tablesError,
|
|
} = useTablesQuery({ projectRef, connectionString: project?.connectionString })
|
|
|
|
// The `supabase_warehouse` publication is the source of truth for what's currently replicated.
|
|
// Reading the sources query directly (rather than via useReplicationSourceId) to get its
|
|
// loading state: the publications query stays disabled until a source id exists, so without it
|
|
// the list would render un-checked and then flash back to a loader once publications kick in.
|
|
const {
|
|
data: sourcesData,
|
|
isLoading: isSourcesLoading,
|
|
isError: isSourcesError,
|
|
error: sourcesError,
|
|
} = useReplicationSourcesQuery({ projectRef })
|
|
const sourceId = sourcesData?.sources.find((source) => source.name === projectRef)?.id
|
|
|
|
const {
|
|
data: publication,
|
|
isError: isPublicationsError,
|
|
error: publicationsError,
|
|
} = useReplicationPublicationQuery({
|
|
projectRef,
|
|
sourceId,
|
|
publicationName: WAREHOUSE_PUBLICATION_NAME,
|
|
})
|
|
|
|
// Derived from data presence rather than fetch status, so there's no render gap between the
|
|
// publication query becoming enabled and it actually starting to fetch.
|
|
const isSelectionPending =
|
|
isSourcesLoading ||
|
|
(sourceId !== undefined && publication === undefined && !isPublicationsError)
|
|
|
|
const initialSelection = useMemo(
|
|
() => buildSelectionFromPublicationTables(publication?.tables ?? []),
|
|
[publication]
|
|
)
|
|
|
|
const selection = selectionOverride ?? initialSelection
|
|
|
|
const schemasWithTables: SchemaWithTables[] = useMemo(() => {
|
|
if (!schemas || !tables) return []
|
|
return schemas
|
|
.filter((schema) => isSelectableWarehouseSchema(schema.name))
|
|
.map((schema) => ({
|
|
schema: schema.name,
|
|
tables: tables.filter((table) => table.schema === schema.name).map((table) => table.name),
|
|
}))
|
|
.sort((a, b) => a.schema.localeCompare(b.schema))
|
|
}, [schemas, tables])
|
|
|
|
const selectedCount = getSelectedTableCount(selection)
|
|
const hasChanges = hasSelectionChanged(selection, initialSelection)
|
|
|
|
const updateSelection = (updater: (current: SchemaTableSelection) => SchemaTableSelection) => {
|
|
setSelectionOverride((prev) => updater(prev ?? initialSelection))
|
|
}
|
|
|
|
const toggleTable = (schema: string, table: string) => {
|
|
const key = getSchemaTableKey(schema, table)
|
|
if (isEditing && initialSelection[key]) return
|
|
updateSelection((current) => ({ ...current, [key]: !current[key] }))
|
|
}
|
|
|
|
const toggleSchema = (schema: SchemaWithTables) => {
|
|
const selectableTables = schema.tables.filter(
|
|
(table) => !isEditing || !initialSelection[getSchemaTableKey(schema.schema, table)]
|
|
)
|
|
const areAllSelectableTablesSelected =
|
|
selectableTables.length > 0 &&
|
|
selectableTables.every((table) => selection[getSchemaTableKey(schema.schema, table)])
|
|
|
|
updateSelection((current) => {
|
|
const next = { ...current }
|
|
selectableTables.forEach((table) => {
|
|
next[getSchemaTableKey(schema.schema, table)] = !areAllSelectableTablesSelected
|
|
})
|
|
return next
|
|
})
|
|
}
|
|
|
|
const setExpanded = (schemaName: string, isOpen: boolean) => {
|
|
setExpandedOverrides((prev) => ({ ...prev, [schemaName]: isOpen }))
|
|
}
|
|
|
|
const handleSubmit = () => {
|
|
const targets = buildWarehouseSetupTargets(selection, schemasWithTables)
|
|
if (targets.length === 0) return
|
|
onSubmit(targets)
|
|
}
|
|
|
|
// Waiting on the publication too, so the pre-checked selection is in place before the user can
|
|
// start toggling (an early toggle would otherwise pin an override that omits existing tables).
|
|
if (isSchemasPending || isTablesPending || isSelectionPending) {
|
|
return (
|
|
<PageSection className="pt-0!">
|
|
<PageSectionMeta>
|
|
<PageSectionSummary>
|
|
<PageSectionTitle>Tables</PageSectionTitle>
|
|
<PageSectionDescription>
|
|
{isEditing
|
|
? 'Add more schemas or tables to Warehouse.'
|
|
: 'Choose which schemas or tables to replicate.'}
|
|
</PageSectionDescription>
|
|
</PageSectionSummary>
|
|
</PageSectionMeta>
|
|
<PageSectionContent>
|
|
<Card>
|
|
<CardContent>
|
|
<GenericSkeletonLoader />
|
|
</CardContent>
|
|
</Card>
|
|
</PageSectionContent>
|
|
</PageSection>
|
|
)
|
|
}
|
|
if (isSchemasError) return <AlertError subject="Failed to load schemas" error={schemasError} />
|
|
if (isTablesError) return <AlertError subject="Failed to load tables" error={tablesError} />
|
|
if (isEditing && isSourcesError) {
|
|
return <AlertError subject="Failed to load replicated tables" error={sourcesError} />
|
|
}
|
|
// Only blocking when editing: a first-time setup starts from an empty selection anyway, so a
|
|
// failed publication lookup shouldn't stop the user from enabling Warehouse at all.
|
|
if (isEditing && isPublicationsError) {
|
|
return <AlertError subject="Failed to load replicated tables" error={publicationsError} />
|
|
}
|
|
|
|
return (
|
|
<PageSection className="pt-0!">
|
|
<PageSectionMeta>
|
|
<PageSectionSummary>
|
|
<PageSectionTitle>Tables</PageSectionTitle>
|
|
<PageSectionDescription>
|
|
{isEditing
|
|
? 'Add more schemas or tables to Warehouse. Tables already replicating cannot be removed yet.'
|
|
: 'Choose which schemas or tables to replicate.'}
|
|
</PageSectionDescription>
|
|
</PageSectionSummary>
|
|
</PageSectionMeta>
|
|
<PageSectionContent className="space-y-4">
|
|
{!!error && (
|
|
<AlertError
|
|
subject={isEditing ? 'Failed to add replicated tables' : 'Failed to enable Warehouse'}
|
|
error={error}
|
|
/>
|
|
)}
|
|
<Card>
|
|
<CardContent className="space-y-3">
|
|
<p className="text-sm font-medium text-foreground-light">
|
|
Schemas and tables to replicate
|
|
</p>
|
|
<div className="overflow-hidden rounded-md border divide-y">
|
|
{schemasWithTables.map((schema) => {
|
|
const keys = schema.tables.map((table) => getSchemaTableKey(schema.schema, table))
|
|
const checkedCount = keys.filter((key) => selection[key]).length
|
|
const checkedState = getSchemaCheckedState({
|
|
selectedCount: checkedCount,
|
|
totalCount: keys.length,
|
|
})
|
|
const isOpen = expandedOverrides[schema.schema] ?? checkedCount > 0
|
|
const selectableTableCount = schema.tables.filter(
|
|
(table) =>
|
|
!isEditing || !initialSelection[getSchemaTableKey(schema.schema, table)]
|
|
).length
|
|
|
|
return (
|
|
<Collapsible
|
|
key={schema.schema}
|
|
open={isOpen}
|
|
onOpenChange={(open) => setExpanded(schema.schema, open)}
|
|
>
|
|
<div className="flex items-center gap-2 px-3 py-2 bg-surface-75">
|
|
<CollapsibleTrigger
|
|
aria-label={
|
|
isOpen ? `Collapse ${schema.schema}` : `Expand ${schema.schema}`
|
|
}
|
|
className="group text-foreground-lighter"
|
|
>
|
|
<ChevronRight
|
|
size={14}
|
|
className="transition-transform group-data-[state=open]:rotate-90"
|
|
/>
|
|
</CollapsibleTrigger>
|
|
<Checkbox
|
|
checked={checkedState}
|
|
onCheckedChange={() => toggleSchema(schema)}
|
|
disabled={selectableTableCount === 0}
|
|
aria-label={`Select all tables in ${schema.schema}`}
|
|
// The shared Checkbox only fills itself for `data-state=checked`, so a partial
|
|
// selection would otherwise render identically to an empty one. A muted fill
|
|
// keeps all three states visually distinct.
|
|
className="data-[state=indeterminate]:border-foreground-lighter data-[state=indeterminate]:bg-foreground-lighter"
|
|
/>
|
|
<span className="text-sm font-mono text-foreground">{schema.schema}</span>
|
|
<span className="text-xs text-foreground-lighter ml-auto">
|
|
{checkedCount}/{keys.length} tables
|
|
</span>
|
|
</div>
|
|
<CollapsibleContent>
|
|
{schema.tables.map((table) => {
|
|
const key = getSchemaTableKey(schema.schema, table)
|
|
return (
|
|
<div key={key} className="flex items-center gap-2 pl-10 pr-3 py-2">
|
|
<Checkbox
|
|
checked={!!selection[key]}
|
|
disabled={isEditing && !!initialSelection[key]}
|
|
onCheckedChange={() => toggleTable(schema.schema, table)}
|
|
aria-label={`Select ${schema.schema}.${table}`}
|
|
/>
|
|
<span className="text-sm font-mono text-foreground-light">{table}</span>
|
|
</div>
|
|
)
|
|
})}
|
|
{schema.tables.length === 0 && (
|
|
<p className="pl-10 pr-3 py-2 text-sm text-foreground-lighter">
|
|
No tables in this schema.
|
|
</p>
|
|
)}
|
|
</CollapsibleContent>
|
|
</Collapsible>
|
|
)
|
|
})}
|
|
</div>
|
|
</CardContent>
|
|
<CardFooter className="justify-between">
|
|
<span className="text-sm text-foreground-lighter">
|
|
{selectedCount} table{selectedCount === 1 ? '' : 's'} selected
|
|
</span>
|
|
<div className="flex items-center gap-x-2">
|
|
{hasChanges && <Button onClick={() => setSelectionOverride(null)}>Cancel</Button>}
|
|
<Button
|
|
variant="primary"
|
|
disabled={selectedCount === 0 || !hasChanges}
|
|
loading={isSubmitting}
|
|
onClick={handleSubmit}
|
|
>
|
|
{isEditing ? 'Add replicated tables' : 'Enable Warehouse'}
|
|
</Button>
|
|
</div>
|
|
</CardFooter>
|
|
</Card>
|
|
</PageSectionContent>
|
|
</PageSection>
|
|
)
|
|
}
|