Files
supabase/apps/studio/components/interfaces/Integrations/Warehouse/WarehouseDisableCard.tsx
5bdfb8743c fix(telemetry): give warehouse_disabled the same schema and table counts as warehouse_enabled (#50643)
<!-- 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>
2026-09-21 20:58:13 +08:00

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>
)
}