mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
chore(studio): Replication UI updates for new replica CTA + URL query state handling (#42378)
## Context Related to unifying read replicas into database replication page which is currently in internal testing ## Changes involved - Updates the "New replica" CTA in `DatabaseSelector` (e.g within the SQL Editor) to link to the replication page instead of settings infrastructure - Refactor replication UI to use URL query states for new destination + edit destination ## To test - [ ] Verify that the "New replica" CTA links correctly to replication page if flag is on - [ ] Verify that new + edit destination UI works as expected - Note that there's some server side validation for ETL replication now so might be tricky to test creating an actual ETL replication - Minimally can test creating a read replica and ensure that the UI behaviour is all as expected <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added unified replication experience with URL-based state management for destination creation and editing * **Refactor** * Simplified replication panel component interfaces and consolidated destination data fetching logic * Enhanced edit flows to leverage URL parameters for seamless navigation * **Chores** * Marked legacy read replica creation panel as deprecated <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
8233902aef
commit
6ffc3fc94c
11 files changed
+257
-165
No files matched your search
+64
-37
@@ -1,8 +1,10 @@
|
||||
import { useFlag, useParams } from 'common'
|
||||
import { useFlag } from 'common'
|
||||
import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
|
||||
import { ArrowUpRight } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { parseAsInteger, parseAsStringEnum, useQueryState } from 'nuqs'
|
||||
import { useEffect } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Button,
|
||||
DialogSectionSeparator,
|
||||
@@ -16,52 +18,81 @@ import {
|
||||
} from 'ui'
|
||||
|
||||
import { EnableReplicationCallout } from '../EnableReplicationCallout'
|
||||
import { PipelineStatusName } from '../Replication.constants'
|
||||
import { useDestinationInformation } from '../useDestinationInformation'
|
||||
import { useIsETLPrivateAlpha } from '../useIsETLPrivateAlpha'
|
||||
import { DestinationForm } from './DestinationForm'
|
||||
import { DestinationType } from './DestinationPanel.types'
|
||||
import { DestinationTypeSelection } from './DestinationTypeSelection'
|
||||
import { ReadReplicaForm } from './ReadReplicaForm'
|
||||
import { DocsButton } from '@/components/ui/DocsButton'
|
||||
import { useReplicationSourcesQuery } from '@/data/replication/sources-query'
|
||||
import { DOCS_URL } from '@/lib/constants'
|
||||
|
||||
interface DestinationPanelProps {
|
||||
visible: boolean
|
||||
type?: DestinationType
|
||||
existingDestination?: {
|
||||
sourceId?: number
|
||||
destinationId: number
|
||||
pipelineId?: number
|
||||
enabled: boolean
|
||||
statusName?: string
|
||||
}
|
||||
onClose: () => void
|
||||
onSuccessCreateReadReplica?: () => void
|
||||
}
|
||||
|
||||
export const DestinationPanel = ({
|
||||
visible,
|
||||
type,
|
||||
existingDestination,
|
||||
onClose,
|
||||
onSuccessCreateReadReplica,
|
||||
}: DestinationPanelProps) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
export const DestinationPanel = ({ onSuccessCreateReadReplica }: DestinationPanelProps) => {
|
||||
const enablePgReplicate = useIsETLPrivateAlpha()
|
||||
const unifiedReplication = useFlag('unifiedReplication')
|
||||
const { hasAccess: hasETLReplicationAccess } = useCheckEntitlements('replication.etl')
|
||||
|
||||
const [selectedType, setSelectedType] = useState<DestinationType>(
|
||||
type || (unifiedReplication ? 'Read Replica' : 'BigQuery')
|
||||
const [urlDestinationType, setDestinationType] = useQueryState(
|
||||
'type',
|
||||
parseAsStringEnum<DestinationType>([
|
||||
'Read Replica',
|
||||
'BigQuery',
|
||||
'Analytics Bucket',
|
||||
]).withOptions({
|
||||
history: 'push',
|
||||
clearOnDefault: true,
|
||||
})
|
||||
)
|
||||
|
||||
const editMode = !!existingDestination
|
||||
const [edit, setEdit] = useQueryState(
|
||||
'edit',
|
||||
parseAsInteger.withOptions({
|
||||
history: 'push',
|
||||
clearOnDefault: true,
|
||||
})
|
||||
)
|
||||
|
||||
const { data: sourcesData, isSuccess: isSourcesSuccess } = useReplicationSourcesQuery({
|
||||
projectRef,
|
||||
})
|
||||
const sourceId = sourcesData?.sources.find((s) => s.name === projectRef)?.id
|
||||
const replicationNotEnabled = isSourcesSuccess && !sourceId
|
||||
const visible = urlDestinationType !== null || edit !== null
|
||||
const editMode = edit !== null
|
||||
|
||||
const {
|
||||
sourceId,
|
||||
pipeline,
|
||||
statusName,
|
||||
replicationNotEnabled,
|
||||
type: existingDestinationType,
|
||||
destinationFetcher,
|
||||
} = useDestinationInformation({ id: edit })
|
||||
const destinationType = existingDestinationType ?? urlDestinationType
|
||||
const invalidExistingDestination = destinationFetcher.error?.code === 404
|
||||
|
||||
const existingDestination = editMode
|
||||
? {
|
||||
sourceId,
|
||||
destinationId: edit,
|
||||
pipelineId: pipeline?.id,
|
||||
statusName,
|
||||
enabled:
|
||||
statusName === PipelineStatusName.STARTED || statusName === PipelineStatusName.FAILED,
|
||||
}
|
||||
: undefined
|
||||
|
||||
const onClose = () => {
|
||||
setDestinationType(null)
|
||||
setEdit(null)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (edit !== null && invalidExistingDestination) {
|
||||
toast(`Unable to find destination ID ${edit}`)
|
||||
setEdit(null)
|
||||
}
|
||||
}, [edit, invalidExistingDestination, setEdit])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -81,15 +112,11 @@ export const DestinationPanel = ({
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<DestinationTypeSelection
|
||||
editMode={editMode}
|
||||
selectedType={selectedType}
|
||||
setSelectedType={setSelectedType}
|
||||
/>
|
||||
<DestinationTypeSelection />
|
||||
|
||||
<DialogSectionSeparator />
|
||||
|
||||
{selectedType === 'Read Replica' ? (
|
||||
{destinationType === 'Read Replica' ? (
|
||||
<ReadReplicaForm onClose={onClose} onSuccess={() => onSuccessCreateReadReplica?.()} />
|
||||
) : unifiedReplication && !enablePgReplicate ? (
|
||||
<SheetSection>
|
||||
@@ -124,14 +151,14 @@ export const DestinationPanel = ({
|
||||
<SheetSection>
|
||||
<EnableReplicationCallout
|
||||
className="!p-6"
|
||||
type={selectedType}
|
||||
type={destinationType}
|
||||
hasAccess={hasETLReplicationAccess}
|
||||
/>
|
||||
</SheetSection>
|
||||
) : (
|
||||
<DestinationForm
|
||||
visible={visible}
|
||||
selectedType={selectedType}
|
||||
selectedType={destinationType ?? 'Read Replica'}
|
||||
existingDestination={existingDestination}
|
||||
onClose={onClose}
|
||||
/>
|
||||
|
||||
+32
-17
@@ -1,22 +1,14 @@
|
||||
import { useFlag } from 'common'
|
||||
import { AnalyticsBucket, BigQuery, Database } from 'icons'
|
||||
import { parseAsInteger, parseAsStringEnum, useQueryState } from 'nuqs'
|
||||
import { Badge, RadioGroupStacked, RadioGroupStackedItem, cn } from 'ui'
|
||||
|
||||
import { useDestinationInformation } from '../useDestinationInformation'
|
||||
import { useIsETLPrivateAlpha } from '../useIsETLPrivateAlpha'
|
||||
import { DestinationType } from './DestinationPanel.types'
|
||||
import { InlineLink } from '@/components/ui/InlineLink'
|
||||
|
||||
type DestinationTypeSelectionProps = {
|
||||
editMode: boolean
|
||||
selectedType: DestinationType
|
||||
setSelectedType: (value: DestinationType) => void
|
||||
}
|
||||
|
||||
export const DestinationTypeSelection = ({
|
||||
editMode,
|
||||
selectedType,
|
||||
setSelectedType,
|
||||
}: DestinationTypeSelectionProps) => {
|
||||
export const DestinationTypeSelection = () => {
|
||||
const enablePgReplicate = useIsETLPrivateAlpha()
|
||||
const unifiedReplication = useFlag('unifiedReplication')
|
||||
const etlEnableBigQuery = useFlag('etlEnableBigQuery')
|
||||
@@ -26,6 +18,27 @@ export const DestinationTypeSelection = ({
|
||||
Boolean
|
||||
).length
|
||||
|
||||
const [urlDestinationType, setDestinationType] = useQueryState(
|
||||
'type',
|
||||
parseAsStringEnum<DestinationType>([
|
||||
'Read Replica',
|
||||
'BigQuery',
|
||||
'Analytics Bucket',
|
||||
]).withOptions({
|
||||
history: 'push',
|
||||
clearOnDefault: true,
|
||||
})
|
||||
)
|
||||
|
||||
const [edit] = useQueryState(
|
||||
'edit',
|
||||
parseAsInteger.withOptions({ history: 'push', clearOnDefault: true })
|
||||
)
|
||||
const editMode = edit !== null
|
||||
|
||||
const { type: existingDestinationType } = useDestinationInformation({ id: edit })
|
||||
const destinationType = existingDestinationType ?? urlDestinationType
|
||||
|
||||
return (
|
||||
<div className="px-5 py-5">
|
||||
<div className="flex flex-col gap-y-1 mb-4">
|
||||
@@ -36,8 +49,8 @@ export const DestinationTypeSelection = ({
|
||||
</div>
|
||||
<RadioGroupStacked
|
||||
disabled={editMode}
|
||||
value={selectedType}
|
||||
onValueChange={(value) => setSelectedType(value as DestinationType)}
|
||||
value={destinationType}
|
||||
onValueChange={(value) => setDestinationType(value as DestinationType)}
|
||||
className={cn(
|
||||
'grid [&>button>div]:py-4',
|
||||
numberOfTypes === 3 ? 'grid-cols-3' : numberOfTypes === 2 ? 'grid-cols-2' : 'grid-cols-1',
|
||||
@@ -45,7 +58,8 @@ export const DestinationTypeSelection = ({
|
||||
'[&>button:first-of-type]:!rounded-l-lg [&>button:last-of-type]:!rounded-r-lg'
|
||||
)}
|
||||
>
|
||||
{((!editMode && unifiedReplication) || (editMode && selectedType === 'Read Replica')) && (
|
||||
{((!editMode && unifiedReplication) ||
|
||||
(editMode && destinationType === 'Read Replica')) && (
|
||||
<RadioGroupStackedItem
|
||||
label=""
|
||||
showIndicator={false}
|
||||
@@ -65,7 +79,7 @@ export const DestinationTypeSelection = ({
|
||||
</RadioGroupStackedItem>
|
||||
)}
|
||||
|
||||
{((!editMode && etlEnableBigQuery) || (editMode && selectedType === 'BigQuery')) && (
|
||||
{((!editMode && etlEnableBigQuery) || (editMode && destinationType === 'BigQuery')) && (
|
||||
<RadioGroupStackedItem label="" showIndicator={false} id="BigQuery" value="BigQuery">
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<BigQuery size={20} />
|
||||
@@ -82,7 +96,8 @@ export const DestinationTypeSelection = ({
|
||||
</RadioGroupStackedItem>
|
||||
)}
|
||||
|
||||
{((!editMode && etlEnableIceberg) || (editMode && selectedType === 'Analytics Bucket')) && (
|
||||
{((!editMode && etlEnableIceberg) ||
|
||||
(editMode && destinationType === 'Analytics Bucket')) && (
|
||||
<RadioGroupStackedItem
|
||||
label=""
|
||||
showIndicator={false}
|
||||
@@ -106,7 +121,7 @@ export const DestinationTypeSelection = ({
|
||||
)}
|
||||
</RadioGroupStacked>
|
||||
|
||||
{selectedType !== 'Read Replica' && enablePgReplicate && (
|
||||
{destinationType !== 'Read Replica' && enablePgReplicate && (
|
||||
<p className="mt-3 text-sm text-foreground-light">
|
||||
Replication is in alpha. Expect rapid changes and possible breaking updates.{' '}
|
||||
<InlineLink href="https://github.com/orgs/supabase/discussions/39416">
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { AlertError } from 'components/ui/AlertError'
|
||||
import { useDeleteDestinationPipelineMutation } from 'data/replication/delete-destination-pipeline-mutation'
|
||||
import { useReplicationPipelineReplicationStatusQuery } from 'data/replication/pipeline-replication-status-query'
|
||||
import { useReplicationPipelineStatusQuery } from 'data/replication/pipeline-status-query'
|
||||
import { useReplicationPipelineVersionQuery } from 'data/replication/pipeline-version-query'
|
||||
import { Pipeline } from 'data/replication/pipelines-query'
|
||||
import { useStopPipelineMutation } from 'data/replication/stop-pipeline-mutation'
|
||||
import { AnalyticsBucket, BigQuery, Database } from 'icons'
|
||||
import { Minus } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
PipelineStatusRequestStatus,
|
||||
usePipelineRequestStatus,
|
||||
} from 'state/replication-pipeline-request-status'
|
||||
import type { ResponseError } from 'types'
|
||||
import {
|
||||
Button,
|
||||
TableCell,
|
||||
@@ -27,46 +24,38 @@ import {
|
||||
WarningIcon,
|
||||
} from 'ui'
|
||||
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
import { DeleteDestination } from './DeleteDestination'
|
||||
import { DestinationPanel } from './DestinationPanel/DestinationPanel'
|
||||
import { DestinationType } from './DestinationPanel/DestinationPanel.types'
|
||||
import { getStatusName, PIPELINE_ERROR_MESSAGES } from './Pipeline.utils'
|
||||
import { PIPELINE_ERROR_MESSAGES } from './Pipeline.utils'
|
||||
import { PipelineStatus } from './PipelineStatus'
|
||||
import { PipelineStatusName, STATUS_REFRESH_FREQUENCY_MS } from './Replication.constants'
|
||||
import { RowMenu } from './RowMenu'
|
||||
import { UpdateVersionModal } from './UpdateVersionModal'
|
||||
import { useDestinationInformation } from './useDestinationInformation'
|
||||
|
||||
interface DestinationRowProps {
|
||||
sourceId?: number
|
||||
destinationId: number
|
||||
destinationName: string
|
||||
type?: DestinationType
|
||||
pipeline?: Pipeline
|
||||
error: ResponseError | null
|
||||
isLoading: boolean
|
||||
isError: boolean
|
||||
isSuccess: boolean
|
||||
}
|
||||
|
||||
export const DestinationRow = ({
|
||||
sourceId,
|
||||
destinationId,
|
||||
destinationName,
|
||||
type,
|
||||
pipeline,
|
||||
error: pipelineError,
|
||||
isLoading: isPipelineLoading,
|
||||
isError: isPipelineError,
|
||||
isSuccess: isPipelineSuccess,
|
||||
}: DestinationRowProps) => {
|
||||
export const DestinationRow = ({ destinationId }: DestinationRowProps) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const [showDeleteDestinationForm, setShowDeleteDestinationForm] = useState(false)
|
||||
const [isDeleting, setIsDeleting] = useState(false)
|
||||
const [showEditDestinationPanel, setShowEditDestinationPanel] = useState(false)
|
||||
const [showUpdateVersionModal, setShowUpdateVersionModal] = useState(false)
|
||||
|
||||
const { type, statusName, destination, pipeline, pipelineStatus, pipelineFetcher } =
|
||||
useDestinationInformation({
|
||||
id: destinationId,
|
||||
})
|
||||
const {
|
||||
error: pipelineError,
|
||||
isPending: isPipelineLoading,
|
||||
isError: isPipelineError,
|
||||
isSuccess: isPipelineSuccess,
|
||||
} = pipelineFetcher
|
||||
const destinationName = destination?.name ?? ''
|
||||
|
||||
const {
|
||||
data: pipelineStatusData,
|
||||
error: pipelineStatusError,
|
||||
isPending: isPipelineStatusLoading,
|
||||
isError: isPipelineStatusError,
|
||||
@@ -78,7 +67,7 @@ export const DestinationRow = ({
|
||||
},
|
||||
{ refetchInterval: STATUS_REFRESH_FREQUENCY_MS }
|
||||
)
|
||||
const { getRequestStatus, updatePipelineStatus, setRequestStatus } = usePipelineRequestStatus()
|
||||
const { getRequestStatus, updatePipelineStatus } = usePipelineRequestStatus()
|
||||
const requestStatus = pipeline?.id
|
||||
? getRequestStatus(pipeline.id)
|
||||
: PipelineStatusRequestStatus.None
|
||||
@@ -86,9 +75,6 @@ export const DestinationRow = ({
|
||||
const { mutateAsync: stopPipeline } = useStopPipelineMutation()
|
||||
const { mutateAsync: deleteDestinationPipeline } = useDeleteDestinationPipelineMutation({})
|
||||
|
||||
const pipelineStatus = pipelineStatusData?.status
|
||||
const statusName = getStatusName(pipelineStatus)
|
||||
|
||||
// Fetch table-level replication status to surface errors in list view
|
||||
const { data: replicationStatusData } = useReplicationPipelineReplicationStatusQuery(
|
||||
{ projectRef, pipelineId: pipeline?.id },
|
||||
@@ -177,7 +163,7 @@ export const DestinationRow = ({
|
||||
<ShimmeringLoader />
|
||||
) : (
|
||||
<PipelineStatus
|
||||
pipelineStatus={pipelineStatusData?.status}
|
||||
pipelineStatus={pipelineStatus?.status}
|
||||
error={pipelineStatusError}
|
||||
isLoading={isPipelineStatusLoading}
|
||||
isError={isPipelineStatusError}
|
||||
@@ -218,13 +204,13 @@ export const DestinationRow = ({
|
||||
</Link>
|
||||
</Button>
|
||||
<RowMenu
|
||||
destinationId={destinationId}
|
||||
pipeline={pipeline}
|
||||
pipelineStatus={pipelineStatusData?.status}
|
||||
pipelineStatus={pipelineStatus?.status}
|
||||
error={pipelineStatusError}
|
||||
isLoading={isPipelineStatusLoading}
|
||||
isError={isPipelineStatusError}
|
||||
onDeleteClick={() => setShowDeleteDestinationForm(true)}
|
||||
onEditClick={() => setShowEditDestinationPanel(true)}
|
||||
hasUpdate={hasUpdate}
|
||||
onUpdateClick={() => setShowUpdateVersionModal(true)}
|
||||
/>
|
||||
@@ -241,20 +227,6 @@ export const DestinationRow = ({
|
||||
name={destinationName}
|
||||
/>
|
||||
|
||||
<DestinationPanel
|
||||
visible={showEditDestinationPanel}
|
||||
type={type}
|
||||
onClose={() => setShowEditDestinationPanel(false)}
|
||||
existingDestination={{
|
||||
sourceId,
|
||||
destinationId: destinationId,
|
||||
pipelineId: pipeline?.id,
|
||||
enabled:
|
||||
statusName === PipelineStatusName.STARTED || statusName === PipelineStatusName.FAILED,
|
||||
statusName,
|
||||
}}
|
||||
/>
|
||||
|
||||
<UpdateVersionModal
|
||||
visible={showUpdateVersionModal}
|
||||
pipeline={pipeline}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useReplicationSourcesQuery } from 'data/replication/sources-query'
|
||||
import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements'
|
||||
import { DOCS_URL } from 'lib/constants'
|
||||
import { Plus, Search, X } from 'lucide-react'
|
||||
import { parseAsStringEnum, useQueryState } from 'nuqs'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
@@ -28,6 +29,7 @@ import { Input } from 'ui-patterns/DataInputs/Input'
|
||||
|
||||
import { REPLICA_STATUS } from '../../Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants'
|
||||
import { DestinationPanel } from './DestinationPanel/DestinationPanel'
|
||||
import { DestinationType } from './DestinationPanel/DestinationPanel.types'
|
||||
import { DestinationRow } from './DestinationRow'
|
||||
import { EnableReplicationCallout } from './EnableReplicationCallout'
|
||||
import { PIPELINE_ERROR_MESSAGES } from './Pipeline.utils'
|
||||
@@ -44,9 +46,20 @@ export const Destinations = () => {
|
||||
|
||||
const prefetchedRef = useRef(false)
|
||||
const [filterString, setFilterString] = useState<string>('')
|
||||
const [showNewDestinationPanel, setShowNewDestinationPanel] = useState(false)
|
||||
const [statusRefetchInterval, setStatusRefetchInterval] = useState<number | false>(5000)
|
||||
|
||||
const [_, setDestinationType] = useQueryState(
|
||||
'type',
|
||||
parseAsStringEnum<DestinationType>([
|
||||
'Read Replica',
|
||||
'BigQuery',
|
||||
'Analytics Bucket',
|
||||
]).withOptions({
|
||||
history: 'push',
|
||||
clearOnDefault: true,
|
||||
})
|
||||
)
|
||||
|
||||
const {
|
||||
data: databases = [],
|
||||
error: databasesError,
|
||||
@@ -175,7 +188,7 @@ export const Destinations = () => {
|
||||
<Button
|
||||
type="default"
|
||||
icon={<Plus />}
|
||||
onClick={() => setShowNewDestinationPanel(true)}
|
||||
onClick={() => setDestinationType('Read Replica')}
|
||||
>
|
||||
Add destination
|
||||
</Button>
|
||||
@@ -229,33 +242,9 @@ export const Destinations = () => {
|
||||
)
|
||||
})}
|
||||
|
||||
{filteredDestinations.map((destination) => {
|
||||
const pipeline = pipelinesData?.pipelines.find(
|
||||
(p) => p.destination_id === destination.id
|
||||
)
|
||||
|
||||
const type =
|
||||
'big_query' in destination.config
|
||||
? 'BigQuery'
|
||||
: 'iceberg' in destination.config
|
||||
? 'Analytics Bucket'
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<DestinationRow
|
||||
key={destination.id}
|
||||
sourceId={sourceId}
|
||||
destinationId={destination.id}
|
||||
destinationName={destination.name}
|
||||
type={type}
|
||||
pipeline={pipeline}
|
||||
error={pipelinesError}
|
||||
isLoading={isPipelinesLoading}
|
||||
isError={isPipelinesError}
|
||||
isSuccess={isPipelinesSuccess}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{filteredDestinations.map((destination) => (
|
||||
<DestinationRow key={destination.id} destinationId={destination.id} />
|
||||
))}
|
||||
|
||||
{!isLoading &&
|
||||
filteredDestinations.length === 0 &&
|
||||
@@ -296,7 +285,7 @@ export const Destinations = () => {
|
||||
</p>
|
||||
<Button
|
||||
icon={<Plus />}
|
||||
onClick={() => setShowNewDestinationPanel(true)}
|
||||
onClick={() => setDestinationType('Read Replica')}
|
||||
className="mt-4"
|
||||
>
|
||||
Add destination
|
||||
@@ -306,11 +295,7 @@ export const Destinations = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DestinationPanel
|
||||
visible={showNewDestinationPanel}
|
||||
onClose={() => setShowNewDestinationPanel(false)}
|
||||
onSuccessCreateReadReplica={() => setStatusRefetchInterval(5000)}
|
||||
/>
|
||||
<DestinationPanel onSuccessCreateReadReplica={() => setStatusRefetchInterval(5000)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from 'ui'
|
||||
import { Admonition } from 'ui-patterns'
|
||||
|
||||
import { DestinationType } from './DestinationPanel/DestinationPanel.types'
|
||||
import { DocsButton } from '@/components/ui/DocsButton'
|
||||
import { UpgradePlanButton } from '@/components/ui/UpgradePlanButton'
|
||||
import { DOCS_URL } from '@/lib/constants'
|
||||
@@ -86,7 +87,7 @@ export const EnableReplicationCallout = ({
|
||||
className,
|
||||
hasAccess,
|
||||
}: {
|
||||
type?: string
|
||||
type?: DestinationType | null
|
||||
className?: string
|
||||
hasAccess: boolean
|
||||
}) => {
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { ArrowUpCircle, Edit, MoreVertical, Pause, Play, RotateCcw, Trash } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { ReplicationPipelineStatusData } from 'data/replication/pipeline-status-query'
|
||||
@@ -8,6 +5,9 @@ import { Pipeline } from 'data/replication/pipelines-query'
|
||||
import { useRestartPipelineHelper } from 'data/replication/restart-pipeline-helper'
|
||||
import { useStartPipelineMutation } from 'data/replication/start-pipeline-mutation'
|
||||
import { useStopPipelineMutation } from 'data/replication/stop-pipeline-mutation'
|
||||
import { ArrowUpCircle, Edit, MoreVertical, Pause, Play, RotateCcw, Trash } from 'lucide-react'
|
||||
import { parseAsInteger, useQueryState } from 'nuqs'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
PipelineStatusRequestStatus,
|
||||
usePipelineRequestStatus,
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from 'ui'
|
||||
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
||||
|
||||
import {
|
||||
PIPELINE_DISABLE_ALLOWED_FROM,
|
||||
PIPELINE_ENABLE_ALLOWED_FROM,
|
||||
@@ -31,31 +32,36 @@ import {
|
||||
import { PipelineStatusName } from './Replication.constants'
|
||||
|
||||
interface RowMenuProps {
|
||||
destinationId: number
|
||||
pipeline: Pipeline | undefined
|
||||
pipelineStatus?: ReplicationPipelineStatusData['status']
|
||||
error: ResponseError | null
|
||||
isLoading: boolean
|
||||
isError: boolean
|
||||
onEditClick: () => void
|
||||
onDeleteClick: () => void
|
||||
hasUpdate?: boolean
|
||||
onDeleteClick: () => void
|
||||
onUpdateClick?: () => void
|
||||
}
|
||||
|
||||
export const RowMenu = ({
|
||||
destinationId,
|
||||
pipeline,
|
||||
pipelineStatus,
|
||||
error,
|
||||
isLoading,
|
||||
isError,
|
||||
onEditClick,
|
||||
onDeleteClick,
|
||||
hasUpdate = false,
|
||||
onDeleteClick,
|
||||
onUpdateClick,
|
||||
}: RowMenuProps) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const statusName = getStatusName(pipelineStatus)
|
||||
|
||||
const [_, setEdit] = useQueryState(
|
||||
'edit',
|
||||
parseAsInteger.withOptions({ history: 'push', clearOnDefault: true })
|
||||
)
|
||||
|
||||
const { mutateAsync: startPipeline } = useStartPipelineMutation()
|
||||
const { mutateAsync: stopPipeline } = useStopPipelineMutation()
|
||||
const { restartPipeline } = useRestartPipelineHelper()
|
||||
@@ -176,7 +182,7 @@ export const RowMenu = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem className="space-x-2" onClick={onEditClick}>
|
||||
<DropdownMenuItem className="space-x-2" onClick={() => setEdit(destinationId)}>
|
||||
<Edit size={14} />
|
||||
<p>Edit destination</p>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { useParams } from 'common'
|
||||
|
||||
import { DestinationType } from './DestinationPanel/DestinationPanel.types'
|
||||
import { getStatusName } from './Pipeline.utils'
|
||||
import { useReplicationDestinationByIdQuery } from '@/data/replication/destination-by-id-query'
|
||||
import { useReplicationPipelineStatusQuery } from '@/data/replication/pipeline-status-query'
|
||||
import { useReplicationPipelinesQuery } from '@/data/replication/pipelines-query'
|
||||
import { useReplicationSourcesQuery } from '@/data/replication/sources-query'
|
||||
|
||||
export const useDestinationInformation = ({ id }: { id?: number | null }) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
const { data: sourcesData, isSuccess: isSourcesSuccess } = useReplicationSourcesQuery({
|
||||
projectRef,
|
||||
})
|
||||
const sourceId = sourcesData?.sources.find((s) => s.name === projectRef)?.id
|
||||
const replicationNotEnabled = isSourcesSuccess && !sourceId
|
||||
|
||||
const {
|
||||
data: destination,
|
||||
error: destinationError,
|
||||
isPending: isDestinationPending,
|
||||
isError: isDestinationError,
|
||||
isSuccess: isDestinationSuccess,
|
||||
} = useReplicationDestinationByIdQuery({
|
||||
projectRef,
|
||||
destinationId: id,
|
||||
})
|
||||
const destinationType: DestinationType | undefined = !destination
|
||||
? undefined
|
||||
: 'big_query' in destination.config
|
||||
? 'BigQuery'
|
||||
: 'iceberg' in destination.config
|
||||
? 'Analytics Bucket'
|
||||
: undefined
|
||||
|
||||
const {
|
||||
data: pipelinesData,
|
||||
error: pipelineError,
|
||||
isPending: isPipelinePending,
|
||||
isError: isPipelineError,
|
||||
isSuccess: isPipelineSuccess,
|
||||
} = useReplicationPipelinesQuery({ projectRef })
|
||||
const pipeline = pipelinesData?.pipelines.find((p) => p.destination_id === id)
|
||||
|
||||
const { data: pipelineStatus } = useReplicationPipelineStatusQuery({
|
||||
projectRef,
|
||||
pipelineId: pipeline?.id,
|
||||
})
|
||||
const statusName = getStatusName(pipelineStatus?.status)
|
||||
|
||||
return {
|
||||
sourceId,
|
||||
destination,
|
||||
pipeline,
|
||||
pipelineStatus,
|
||||
// Derivatives
|
||||
statusName,
|
||||
type: destinationType,
|
||||
replicationNotEnabled,
|
||||
// Data fetching status (Secondary information)
|
||||
pipelineFetcher: {
|
||||
error: pipelineError,
|
||||
isPending: isPipelinePending,
|
||||
isError: isPipelineError,
|
||||
isSuccess: isPipelineSuccess,
|
||||
},
|
||||
destinationFetcher: {
|
||||
error: destinationError,
|
||||
isPending: isDestinationPending,
|
||||
isError: isDestinationError,
|
||||
isSuccess: isDestinationSuccess,
|
||||
},
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -1,5 +1,8 @@
|
||||
import { useQueryState, parseAsBoolean } from 'nuqs'
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs'
|
||||
|
||||
/**
|
||||
* @deprecated Can remove after read replicas is fully moved into database replication page
|
||||
*/
|
||||
export function useShowNewReplicaPanel() {
|
||||
const [showNewReplicaPanel, setShowNewReplicaPanel] = useQueryState(
|
||||
'createReplica',
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
import { noop } from 'lodash'
|
||||
import { Check, ChevronDown, Loader2, Plus } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useFlag, useParams } from 'common'
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
import { REPLICA_STATUS } from 'components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants'
|
||||
import { useShowNewReplicaPanel } from 'components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/use-show-new-replica'
|
||||
@@ -14,6 +7,12 @@ import { formatDatabaseID, formatDatabaseRegion } from 'data/read-replicas/repli
|
||||
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
|
||||
import { IS_PLATFORM } from 'lib/constants'
|
||||
import { timeout } from 'lib/helpers'
|
||||
import { noop } from 'lodash'
|
||||
import { Check, ChevronDown, Loader2, Plus } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
|
||||
import {
|
||||
Button,
|
||||
@@ -57,6 +56,7 @@ export const DatabaseSelector = ({
|
||||
const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
|
||||
const { setShowNewReplicaPanel } = useShowNewReplicaPanel()
|
||||
|
||||
const unifiedReplication = useFlag('unifiedReplication')
|
||||
const { infrastructureReadReplicas } = useIsFeatureEnabled(['infrastructure:read_replicas'])
|
||||
|
||||
const state = useDatabaseSelectorStateSnapshot()
|
||||
@@ -74,6 +74,10 @@ export const DatabaseSelector = ({
|
||||
|
||||
const selectedAdditionalOption = additionalOptions.find((x) => x.id === selectedDatabaseId)
|
||||
|
||||
const newReplicaURL = unifiedReplication
|
||||
? `/project/${projectRef}/database/replication?type=Read+Replica`
|
||||
: `/project/${projectRef}/settings/infrastructure`
|
||||
|
||||
useEffect(() => {
|
||||
if (_selectedDatabaseId) state.setSelectedDatabaseId(_selectedDatabaseId)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -215,12 +219,12 @@ export const DatabaseSelector = ({
|
||||
className="cursor-pointer w-full"
|
||||
onSelect={() => {
|
||||
setOpen(false)
|
||||
router.push(`/project/${projectRef}/settings/infrastructure`)
|
||||
router.push(newReplicaURL)
|
||||
}}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<Link
|
||||
href={`/project/${projectRef}/settings/infrastructure`}
|
||||
href={newReplicaURL}
|
||||
onClick={async () => {
|
||||
setOpen(false)
|
||||
// [Joshen] This is used in the Connect UI which is available across all pages
|
||||
@@ -230,7 +234,7 @@ export const DatabaseSelector = ({
|
||||
// the replica panel from a "portal" based component (e.g dialog, sheet, dropdown, etc)
|
||||
// Although I'd prefer if there's a better way to resolve this
|
||||
await timeout(50)
|
||||
setShowNewReplicaPanel(true)
|
||||
if (!unifiedReplication) setShowNewReplicaPanel(true)
|
||||
}}
|
||||
className="w-full flex items-center gap-2"
|
||||
>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
import type { components } from 'api-types'
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import type { ResponseError, UseCustomQueryOptions } from 'types'
|
||||
|
||||
import { replicationKeys } from './keys'
|
||||
|
||||
type ReplicationDestinationByIdParams = { projectRef?: string; destinationId?: number }
|
||||
type ReplicationDestinationByIdParams = { projectRef?: string; destinationId?: number | null }
|
||||
|
||||
async function fetchReplicationDestinationById(
|
||||
{ projectRef, destinationId }: ReplicationDestinationByIdParams,
|
||||
@@ -36,6 +36,10 @@ export const useReplicationDestinationByIdQuery = <TData = ReplicationDestinatio
|
||||
useQuery<ReplicationDestinationByIdData, ResponseError, TData>({
|
||||
queryKey: replicationKeys.destinationById(projectRef, destinationId),
|
||||
queryFn: ({ signal }) => fetchReplicationDestinationById({ projectRef, destinationId }, signal),
|
||||
enabled: enabled && typeof projectRef !== 'undefined' && typeof destinationId !== 'undefined',
|
||||
enabled:
|
||||
enabled &&
|
||||
typeof projectRef !== 'undefined' &&
|
||||
typeof destinationId !== 'undefined' &&
|
||||
destinationId !== null,
|
||||
...options,
|
||||
})
|
||||
@@ -2,7 +2,7 @@ export const replicationKeys = {
|
||||
sources: (projectRef: string | undefined) => ['projects', projectRef, 'sources'] as const,
|
||||
destinations: (projectRef: string | undefined) =>
|
||||
['projects', projectRef, 'destinations'] as const,
|
||||
destinationById: (projectRef: string | undefined, destinationId: number | undefined) =>
|
||||
destinationById: (projectRef: string | undefined, destinationId: number | undefined | null) =>
|
||||
['projects', projectRef, 'destinations', destinationId] as const,
|
||||
publications: (projectRef: string | undefined, source_id: number | undefined) =>
|
||||
['projects', projectRef, 'sources', source_id, 'publications'] as const,
|
||||
|
||||
Reference in new issue
Block a user