mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
<!-- ccr-slack-attribution --> _Requested by **Pam Chia** · [Slack thread](https://supabase.slack.com/archives/C076KTY11DF/p1789979663384919?thread_ts=1789953229.116889&cid=C076KTY11DF)_ ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Bug fix (telemetry). ## What is the current behavior? **Before:** Disabling Warehouse fires `warehouse_disabled` with no properties at all, while enabling it fires `warehouse_enabled` with `schemaTargetCount` and `tableTargetCount`. Disables can be counted, but nothing says how much was being replicated when the user turned it off, so churn cannot be segmented by the size or shape of the setup being torn down. ## What is the new behavior? **After:** `warehouse_disabled` carries `schemaTargetCount` and `tableTargetCount` with exactly the same meaning they have on `warehouse_enabled`: schemas replicated in full, and tables replicated individually on top of those. A disable of a project replicating one whole schema plus two loose tables now reports one schema target and two table targets, so enable and disable volume line up on the same two properties. ## Additional context **How:** The counts are read once, when the user confirms the dialog, and held in a ref until the mutation succeeds. The setup mutation's own `onSuccess` invalidates the setup-status and replication-sources queries and awaits those refetches before the caller's callback runs, so anything read inside `onSuccess` already reflects the post-disable state and would report nothing replicated. The event is tracked from that hook-level `onSuccess` rather than a `mutateAsync` callback: the status refetch swaps the Disable card out of the panel, and mutate-level callbacks are skipped once the component has unmounted. The shape is reproduced from the `supabase_warehouse` publication through the same helpers the table picker uses — the publication's tables become a selection, and that selection is mapped back to targets against the project's selectable schemas. Counting distinct schemas and tables off the replicated-table list instead would put a different meaning behind the same property names: a fully covered schema would be counted as its individual tables rather than as one schema target, and the two events would no longer be comparable. Both properties are optional. The replicated-table list is assembled from four queries, and when they have not resolved the properties are omitted rather than sent as `0`, so "unknown" is never recorded as "nothing was replicated". Tests: unit tests for the extracted `buildSchemasWithTables` helper, and a component test that drives the disable dialog against a publication covering one schema in full plus one table from another. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_0197pGnhiAkhiiYiRxY3qVFY --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Pamela Chia <pamelachiamayyee@gmail.com>
127 lines
4.6 KiB
TypeScript
127 lines
4.6 KiB
TypeScript
import { useQueryClient } from '@tanstack/react-query'
|
|
import { useParams } from 'common'
|
|
import { useRef, useState } from 'react'
|
|
import { toast } from 'sonner'
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogBody,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
Button,
|
|
Card,
|
|
CardContent,
|
|
} from 'ui'
|
|
import { FormLayout } from 'ui-patterns/form/Layout/FormLayout'
|
|
import {
|
|
PageSection,
|
|
PageSectionContent,
|
|
PageSectionMeta,
|
|
PageSectionSummary,
|
|
PageSectionTitle,
|
|
} from 'ui-patterns/PageSection'
|
|
|
|
import { useWarehouseReplicatedTargets } from './useWarehouseReplicatedTargets'
|
|
import { type WarehouseSetupTarget } from './Warehouse.utils'
|
|
import { warehouseKeys } from '@/data/warehouse/keys'
|
|
import { useWarehouseSetupMutation } from '@/data/warehouse/warehouse-setup-mutation'
|
|
import { useTrack } from '@/lib/telemetry/track'
|
|
|
|
export const WarehouseDisableCard = () => {
|
|
const { ref: projectRef } = useParams()
|
|
const queryClient = useQueryClient()
|
|
const track = useTrack()
|
|
const [isConfirming, setIsConfirming] = useState(false)
|
|
|
|
const replicatedTargets = useWarehouseReplicatedTargets({ projectRef })
|
|
// Snapshotted when the user confirms: the mutation's own onSuccess awaits the post-disable
|
|
// refetches, after which nothing reads as replicated.
|
|
const targetsAtConfirm = useRef<WarehouseSetupTarget[] | undefined>(undefined)
|
|
|
|
const setupMutation = useWarehouseSetupMutation({
|
|
onSuccess: async () => {
|
|
// Tracked from the mutation's onSuccess, not mutateAsync's: the status refetch swaps this
|
|
// card out of the panel, and mutate-level callbacks are skipped once it has unmounted.
|
|
const targets = targetsAtConfirm.current
|
|
track('warehouse_disabled', {
|
|
...(targets !== undefined && {
|
|
schemaTargetCount: targets.filter((target) => target.type === 'schema').length,
|
|
tableTargetCount: targets.filter((target) => target.type === 'table').length,
|
|
}),
|
|
})
|
|
// Disabling also turns off catalog access server-side, which the setup mutation doesn't know
|
|
// to invalidate on its own.
|
|
await queryClient.invalidateQueries({ queryKey: warehouseKeys.catalog(projectRef) })
|
|
setIsConfirming(false)
|
|
toast.success('Warehouse disabled')
|
|
},
|
|
onError: (error) => toast.error(`Failed to disable Warehouse: ${error.message}`),
|
|
})
|
|
|
|
const handleDisable = () => {
|
|
if (!projectRef) return undefined
|
|
targetsAtConfirm.current = replicatedTargets
|
|
// Returned so the dialog stays open on failure and closes once the disable succeeds.
|
|
return setupMutation.mutateAsync({ projectRef, body: { targets: [] } })
|
|
}
|
|
|
|
return (
|
|
<PageSection className="pt-5!">
|
|
<PageSectionMeta>
|
|
<PageSectionSummary>
|
|
<PageSectionTitle>Disable</PageSectionTitle>
|
|
</PageSectionSummary>
|
|
</PageSectionMeta>
|
|
<PageSectionContent>
|
|
<Card>
|
|
<CardContent>
|
|
<FormLayout
|
|
layout="flex-row-reverse"
|
|
label="Disable Warehouse for this project"
|
|
description="Stops replication and removes Warehouse-managed resources."
|
|
>
|
|
<Button variant="danger" onClick={() => setIsConfirming(true)}>
|
|
Disable Warehouse
|
|
</Button>
|
|
</FormLayout>
|
|
</CardContent>
|
|
</Card>
|
|
</PageSectionContent>
|
|
|
|
<AlertDialog open={isConfirming} onOpenChange={setIsConfirming}>
|
|
<AlertDialogContent size="small">
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Disable Warehouse</AlertDialogTitle>
|
|
</AlertDialogHeader>
|
|
<AlertDialogBody>
|
|
<AlertDialogDescription asChild>
|
|
<div className="space-y-2">
|
|
<p>
|
|
Disabling Warehouse stops replication and connections to its endpoint. Its
|
|
pipeline, publication, catalog access, and foreign tables are removed.
|
|
</p>
|
|
<p>Copied data remains in DuckLake storage until deleted.</p>
|
|
</div>
|
|
</AlertDialogDescription>
|
|
</AlertDialogBody>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
variant="danger"
|
|
loading={setupMutation.isPending}
|
|
disabled={!projectRef}
|
|
onClick={handleDisable}
|
|
>
|
|
Disable Warehouse
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</PageSection>
|
|
)
|
|
}
|