DestinationPanel.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import { ArrowUpRight } from 'lucide-react'
  2. import Link from 'next/link'
  3. import { parseAsInteger, parseAsStringEnum, useQueryState } from 'nuqs'
  4. import { useEffect } from 'react'
  5. import { toast } from 'sonner'
  6. import {
  7. Button,
  8. cn,
  9. DialogSectionSeparator,
  10. Sheet,
  11. SheetContent,
  12. SheetDescription,
  13. SheetHeader,
  14. SheetSection,
  15. SheetTitle,
  16. } from 'ui'
  17. import { EnableReplicationCallout } from '../EnableReplicationCallout'
  18. import { PipelineStatusName } from '../Replication.constants'
  19. import { useDestinationInformation } from '../useDestinationInformation'
  20. import { useIsETLPrivateAlpha } from '../useIsETLPrivateAlpha'
  21. import { DestinationForm } from './DestinationForm'
  22. import { DestinationType } from './DestinationPanel.types'
  23. import { DestinationTypeSelection } from './DestinationTypeSelection'
  24. import { ReadReplicaForm } from './ReadReplicaForm'
  25. import { DocsButton } from '@/components/ui/DocsButton'
  26. import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
  27. import { DOCS_URL } from '@/lib/constants'
  28. interface DestinationPanelProps {
  29. onSuccessCreateReadReplica?: () => void
  30. }
  31. export const DestinationPanel = ({ onSuccessCreateReadReplica }: DestinationPanelProps) => {
  32. const enablePgReplicate = useIsETLPrivateAlpha()
  33. const { hasAccess: hasETLReplicationAccess } = useCheckEntitlements('replication.etl')
  34. const [urlDestinationType, setDestinationType] = useQueryState(
  35. 'destinationType',
  36. parseAsStringEnum<DestinationType>([
  37. 'Read Replica',
  38. 'BigQuery',
  39. 'Analytics Bucket',
  40. 'DuckLake',
  41. ]).withOptions({
  42. history: 'push',
  43. clearOnDefault: true,
  44. })
  45. )
  46. const [edit, setEdit] = useQueryState(
  47. 'edit',
  48. parseAsInteger.withOptions({
  49. history: 'push',
  50. clearOnDefault: true,
  51. })
  52. )
  53. const visible = urlDestinationType !== null || edit !== null
  54. const editMode = edit !== null
  55. const {
  56. sourceId,
  57. pipeline,
  58. statusName,
  59. replicationNotEnabled,
  60. type: existingDestinationType,
  61. destinationFetcher,
  62. } = useDestinationInformation({ id: edit })
  63. const destinationType = existingDestinationType ?? urlDestinationType
  64. const invalidExistingDestination = destinationFetcher.error?.code === 404
  65. const existingDestination = editMode
  66. ? {
  67. sourceId,
  68. destinationId: edit,
  69. pipelineId: pipeline?.id,
  70. statusName,
  71. enabled:
  72. statusName === PipelineStatusName.STARTED || statusName === PipelineStatusName.FAILED,
  73. }
  74. : undefined
  75. const onClose = () => {
  76. setDestinationType(null)
  77. setEdit(null)
  78. }
  79. useEffect(() => {
  80. if (edit !== null && invalidExistingDestination) {
  81. toast(`Unable to find destination ID ${edit}`)
  82. setEdit(null)
  83. }
  84. }, [edit, invalidExistingDestination, setEdit])
  85. return (
  86. <>
  87. <Sheet open={visible} onOpenChange={onClose}>
  88. <SheetContent size="default" showClose={false} className="md:w-[850px]!">
  89. <div className="flex flex-col h-full" tabIndex={-1}>
  90. <SheetHeader>
  91. <SheetTitle>{editMode ? 'Edit destination' : 'Create a new destination'}</SheetTitle>
  92. <SheetDescription>
  93. {editMode
  94. ? 'Update the configuration for this destination'
  95. : 'A destination can be a read replica or an external platform that receives your database changes in real time.'}
  96. </SheetDescription>
  97. </SheetHeader>
  98. <DestinationTypeSelection />
  99. <DialogSectionSeparator />
  100. {destinationType === 'Read Replica' ? (
  101. <ReadReplicaForm onClose={onClose} onSuccess={() => onSuccessCreateReadReplica?.()} />
  102. ) : !enablePgReplicate ? (
  103. <SheetSection>
  104. <div className={cn('border rounded-md p-6 flex flex-col gap-y-4')}>
  105. <div className="flex flex-col gap-y-1">
  106. <h4>Replicate data to external destinations in real-time</h4>
  107. <p className="text-sm text-foreground-light">
  108. We are currently in <span className="text-foreground">private alpha</span> and
  109. slowly onboarding new customers to ensure stable data pipelines. Request
  110. access below to join the waitlist. Read replicas are available now.
  111. </p>
  112. </div>
  113. <div className="flex gap-x-2">
  114. <Button
  115. asChild
  116. type="secondary"
  117. iconRight={<ArrowUpRight size={16} strokeWidth={1.5} />}
  118. >
  119. <Link
  120. href="https://forms.supabase.com/pg_replicate"
  121. target="_blank"
  122. rel="noreferrer"
  123. >
  124. Request alpha access
  125. </Link>
  126. </Button>
  127. <DocsButton href={`${DOCS_URL}/guides/database/replication#replication`} />
  128. </div>
  129. </div>
  130. </SheetSection>
  131. ) : replicationNotEnabled ? (
  132. <SheetSection>
  133. <EnableReplicationCallout
  134. className="p-6!"
  135. type={destinationType}
  136. hasAccess={hasETLReplicationAccess}
  137. />
  138. </SheetSection>
  139. ) : (
  140. <DestinationForm
  141. visible={visible}
  142. selectedType={destinationType ?? 'Read Replica'}
  143. existingDestination={existingDestination}
  144. onClose={onClose}
  145. />
  146. )}
  147. </div>
  148. </SheetContent>
  149. </Sheet>
  150. </>
  151. )
  152. }