mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
feat(replication): Implement UI to render pipeline replication status (#37376)
This commit is contained in:
1 parent
cfb22045d0
commit
0ecddf1901
20 files changed
+1401
-574
No files matched your search
@@ -1,13 +1,15 @@
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import { motion } from 'framer-motion'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { ArrowUpRight, Circle, Database, MoreVertical, Plus, Search } from 'lucide-react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import Link from 'next/link'
|
||||
import { useMemo } from 'react'
|
||||
import ReactFlow, { Background, Handle, Position, ReactFlowProvider } from 'reactflow'
|
||||
import 'reactflow/dist/style.css'
|
||||
import { Button, Input } from 'ui'
|
||||
|
||||
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import { BASE_PATH } from 'lib/constants'
|
||||
import { Button, Input_Shadcn_ } from 'ui'
|
||||
import { NODE_WIDTH } from '../../Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants'
|
||||
|
||||
const STATIC_NODES = [
|
||||
@@ -62,6 +64,14 @@ const STATIC_EDGES = [
|
||||
{ id: 'e1-4', source: '1', target: '4', type: 'smoothstep', animated: true },
|
||||
]
|
||||
|
||||
export const ReplicationComingSoon = () => {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<ReplicationStaticMockup />
|
||||
</ReactFlowProvider>
|
||||
)
|
||||
}
|
||||
|
||||
const ReplicationStaticMockup = () => {
|
||||
const nodes = useMemo(() => STATIC_NODES, [])
|
||||
const edges = useMemo(() => STATIC_EDGES, [])
|
||||
@@ -108,16 +118,6 @@ const ReplicationStaticMockup = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const ReplicationComingSoon = () => {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<ReplicationStaticMockup />
|
||||
</ReactFlowProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReplicationComingSoon
|
||||
|
||||
const PrimaryNode = ({
|
||||
data,
|
||||
}: {
|
||||
@@ -251,58 +251,68 @@ const StaticDestinations = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col bg-surface-100 px-6 pt-6 border-t relative h-full">
|
||||
<div className="bg-surface-300 w-full h-full absolute top-0 left-0 opacity-30"></div>
|
||||
<div className="flex items-center justify-between">
|
||||
<Input
|
||||
size="small"
|
||||
className="w-52 bg-transparent"
|
||||
iconContainerClassName="pl-2"
|
||||
icon={<Search size={14} className="text-foreground-lighter" />}
|
||||
placeholder="Search..."
|
||||
/>
|
||||
<Button
|
||||
icon={<Plus size={16} />}
|
||||
type="primary"
|
||||
className="flex items-center"
|
||||
onClick={() => {}}
|
||||
>
|
||||
New destination
|
||||
</Button>
|
||||
</div>
|
||||
<Table
|
||||
head={[
|
||||
<Table.th key="name">Name</Table.th>,
|
||||
<Table.th key="publication">Publication</Table.th>,
|
||||
<Table.th key="lag">Lag</Table.th>,
|
||||
<Table.th key="status">Status</Table.th>,
|
||||
<Table.th key="actions"></Table.th>,
|
||||
]}
|
||||
className="mt-4"
|
||||
body={mockRows.map((row, i) => (
|
||||
<Table.tr key={i}>
|
||||
<Table.td>{row.name}</Table.td>
|
||||
<Table.td>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="font-bold">All</span>
|
||||
<span className="text-sm text-foreground-lighter">{row.tables} tables</span>
|
||||
</span>
|
||||
</Table.td>
|
||||
<Table.td>{row.lag}</Table.td>
|
||||
<Table.td>
|
||||
<span className="flex items-center gap-3">
|
||||
<Circle size={10} className="bg-brand-500 stroke-none rounded-full" />
|
||||
{row.status}
|
||||
</span>
|
||||
</Table.td>
|
||||
<Table.td className="text-right">
|
||||
<button className="p-1">
|
||||
<MoreVertical size={18} />
|
||||
</button>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
))}
|
||||
/>
|
||||
<div className="flex flex-col bg-surface-100 px-6 py-6 border-t relative ">
|
||||
<div className="z-10 bg-surface-300 w-full h-full absolute top-0 left-0 opacity-30" />
|
||||
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldSection className="!py-0">
|
||||
<div className="col-span-12">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="relative w-52">
|
||||
<Search
|
||||
size={14}
|
||||
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-foreground-lighter"
|
||||
/>
|
||||
<Input_Shadcn_
|
||||
className="pl-9 bg-transparent h-8 pointer-events-none"
|
||||
placeholder="Search..."
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
disabled
|
||||
type="primary"
|
||||
icon={<Plus size={16} />}
|
||||
className="flex items-center pointer-events-none"
|
||||
>
|
||||
New destination
|
||||
</Button>
|
||||
</div>
|
||||
<Table
|
||||
head={[
|
||||
<Table.th key="name">Name</Table.th>,
|
||||
<Table.th key="publication">Publication</Table.th>,
|
||||
<Table.th key="lag">Lag</Table.th>,
|
||||
<Table.th key="status">Status</Table.th>,
|
||||
<Table.th key="actions"></Table.th>,
|
||||
]}
|
||||
className="mt-4"
|
||||
body={mockRows.map((row, i) => (
|
||||
<Table.tr key={i}>
|
||||
<Table.td>{row.name}</Table.td>
|
||||
<Table.td>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="font-bold">All</span>
|
||||
<span className="text-sm text-foreground-lighter">{row.tables} tables</span>
|
||||
</span>
|
||||
</Table.td>
|
||||
<Table.td>{row.lag}</Table.td>
|
||||
<Table.td>
|
||||
<span className="flex items-center gap-3">
|
||||
<Circle size={10} className="bg-brand-500 stroke-none rounded-full" />
|
||||
{row.status}
|
||||
</span>
|
||||
</Table.td>
|
||||
<Table.td className="text-right">
|
||||
<button className="p-1">
|
||||
<MoreVertical size={18} />
|
||||
</button>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
))}
|
||||
/>
|
||||
</div>
|
||||
</ScaffoldSection>
|
||||
</ScaffoldContainer>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { useDeleteDestinationMutation } from 'data/replication/delete-destination-mutation'
|
||||
import { useReplicationPipelineStatusQuery } from 'data/replication/pipeline-status-query'
|
||||
import { ReplicationPipelinesData } from 'data/replication/pipelines-query'
|
||||
import { useStopPipelineMutation } from 'data/replication/stop-pipeline-mutation'
|
||||
import {
|
||||
PipelineStatusRequestStatus,
|
||||
usePipelineRequestStatus,
|
||||
} from 'state/replication-pipeline-request-status'
|
||||
import { ResponseError } from 'types'
|
||||
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
|
||||
import RowMenu from './RowMenu'
|
||||
import PipelineStatus, { PipelineStatusRequestStatus, PipelineStatusName } from './PipelineStatus'
|
||||
import { useParams } from 'common'
|
||||
import {
|
||||
ReplicationPipelineStatusData,
|
||||
useReplicationPipelineStatusQuery,
|
||||
} from 'data/replication/pipeline-status-query'
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { useStartPipelineMutation } from 'data/replication/start-pipeline-mutation'
|
||||
import { useStopPipelineMutation } from 'data/replication/stop-pipeline-mutation'
|
||||
import { useDeleteDestinationMutation } from 'data/replication/delete-destination-mutation'
|
||||
import DeleteDestination from './DeleteDestination'
|
||||
import DestinationPanel from './DestinationPanel'
|
||||
import { getStatusName, PIPELINE_ERROR_MESSAGES } from './Pipeline.utils'
|
||||
import { PipelineStatus, PipelineStatusName } from './PipelineStatus'
|
||||
import { RowMenu } from './RowMenu'
|
||||
|
||||
export type Pipeline = ReplicationPipelinesData['pipelines'][0]
|
||||
|
||||
const refreshFrequencyMs: number = 5000
|
||||
const refreshFrequencyMs: number = 2000
|
||||
|
||||
interface DestinationRowProps {
|
||||
sourceId: number | undefined
|
||||
@@ -32,9 +34,10 @@ interface DestinationRowProps {
|
||||
isLoading: boolean
|
||||
isError: boolean
|
||||
isSuccess: boolean
|
||||
onSelectPipeline?: (pipelineId: number, destinationName: string) => void
|
||||
}
|
||||
|
||||
const DestinationRow = ({
|
||||
export const DestinationRow = ({
|
||||
sourceId,
|
||||
destinationId,
|
||||
destinationName,
|
||||
@@ -44,6 +47,7 @@ const DestinationRow = ({
|
||||
isLoading: isPipelineLoading,
|
||||
isError: isPipelineError,
|
||||
isSuccess: isPipelineSuccess,
|
||||
onSelectPipeline,
|
||||
}: DestinationRowProps) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const [showDeleteDestinationForm, setShowDeleteDestinationForm] = useState(false)
|
||||
@@ -62,76 +66,23 @@ const DestinationRow = ({
|
||||
},
|
||||
{ refetchInterval: refreshFrequencyMs }
|
||||
)
|
||||
const [requestStatus, setRequestStatus] = useState<PipelineStatusRequestStatus>(
|
||||
PipelineStatusRequestStatus.None
|
||||
)
|
||||
const { mutateAsync: startPipeline } = useStartPipelineMutation()
|
||||
const { getRequestStatus, updatePipelineStatus } = usePipelineRequestStatus()
|
||||
const requestStatus = pipeline?.id
|
||||
? getRequestStatus(pipeline.id)
|
||||
: PipelineStatusRequestStatus.None
|
||||
|
||||
const { mutateAsync: stopPipeline } = useStopPipelineMutation()
|
||||
const pipelineStatus = pipelineStatusData?.status
|
||||
const getStatusName = (
|
||||
status: ReplicationPipelineStatusData['status'] | undefined
|
||||
): string | undefined => {
|
||||
if (status && typeof status === 'object' && 'name' in status) {
|
||||
return status.name
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const statusName = getStatusName(pipelineStatus)
|
||||
if (
|
||||
(requestStatus === PipelineStatusRequestStatus.EnableRequested &&
|
||||
(statusName === PipelineStatusName.STARTED || statusName === PipelineStatusName.FAILED)) ||
|
||||
(requestStatus === PipelineStatusRequestStatus.DisableRequested &&
|
||||
(statusName === PipelineStatusName.STOPPED || statusName === PipelineStatusName.FAILED))
|
||||
) {
|
||||
setRequestStatus(PipelineStatusRequestStatus.None)
|
||||
}
|
||||
|
||||
const onEnableClick = async () => {
|
||||
if (!projectRef) {
|
||||
console.error('Project ref is required')
|
||||
return
|
||||
}
|
||||
if (!pipeline) {
|
||||
toast.error('No pipeline found')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await startPipeline({ projectRef, pipelineId: pipeline.id })
|
||||
} catch (error) {
|
||||
toast.error('Failed to enable destination')
|
||||
}
|
||||
setRequestStatus(PipelineStatusRequestStatus.EnableRequested)
|
||||
}
|
||||
const onDisableClick = async () => {
|
||||
if (!projectRef) {
|
||||
console.error('Project ref is required')
|
||||
return
|
||||
}
|
||||
if (!pipeline) {
|
||||
toast.error('No pipeline found')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await stopPipeline({ projectRef, pipelineId: pipeline.id })
|
||||
} catch (error) {
|
||||
toast.error('Failed to disable destination')
|
||||
}
|
||||
setRequestStatus(PipelineStatusRequestStatus.DisableRequested)
|
||||
}
|
||||
const { mutateAsync: deleteDestination } = useDeleteDestinationMutation({})
|
||||
|
||||
const pipelineStatus = pipelineStatusData?.status
|
||||
const statusName = getStatusName(pipelineStatus)
|
||||
|
||||
const onDeleteClick = async () => {
|
||||
if (!projectRef) {
|
||||
console.error('Project ref is required')
|
||||
return
|
||||
return console.error('Project ref is required')
|
||||
}
|
||||
if (!pipeline) {
|
||||
toast.error('No pipeline found')
|
||||
return
|
||||
return toast.error(PIPELINE_ERROR_MESSAGES.NO_PIPELINE_FOUND)
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -140,24 +91,33 @@ const DestinationRow = ({
|
||||
// so we don't need to call deletePipeline explicitly
|
||||
await deleteDestination({ projectRef, destinationId: destinationId })
|
||||
} catch (error) {
|
||||
toast.error('Failed to delete destination')
|
||||
toast.error(PIPELINE_ERROR_MESSAGES.DELETE_DESTINATION)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (pipeline?.id) {
|
||||
updatePipelineStatus(pipeline.id, statusName)
|
||||
}
|
||||
}, [pipeline?.id, statusName, updatePipelineStatus])
|
||||
|
||||
return (
|
||||
<>
|
||||
{isPipelineError && (
|
||||
<AlertError error={pipelineError} subject="Failed to retrieve pipeline" />
|
||||
<AlertError error={pipelineError} subject={PIPELINE_ERROR_MESSAGES.RETRIEVE_PIPELINE} />
|
||||
)}
|
||||
{isPipelineSuccess && (
|
||||
<Table.tr>
|
||||
<Table.td>
|
||||
{isPipelineLoading ? <ShimmeringLoader></ShimmeringLoader> : destinationName}
|
||||
</Table.td>
|
||||
<Table.td>{isPipelineLoading ? <ShimmeringLoader></ShimmeringLoader> : type}</Table.td>
|
||||
<Table.tr
|
||||
className="hover:!bg-surface-200 transition"
|
||||
onClick={() => {
|
||||
if (pipeline) onSelectPipeline?.(pipeline.id, destinationName)
|
||||
}}
|
||||
>
|
||||
<Table.td>{isPipelineLoading ? <ShimmeringLoader /> : destinationName}</Table.td>
|
||||
<Table.td>{isPipelineLoading ? <ShimmeringLoader /> : type}</Table.td>
|
||||
<Table.td>
|
||||
{isPipelineLoading || !pipeline ? (
|
||||
<ShimmeringLoader></ShimmeringLoader>
|
||||
<ShimmeringLoader />
|
||||
) : (
|
||||
<PipelineStatus
|
||||
pipelineStatus={pipelineStatusData?.status}
|
||||
@@ -166,27 +126,28 @@ const DestinationRow = ({
|
||||
isError={isPipelineStatusError}
|
||||
isSuccess={isPipelineStatusSuccess}
|
||||
requestStatus={requestStatus}
|
||||
></PipelineStatus>
|
||||
/>
|
||||
)}
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
{isPipelineLoading || !pipeline ? (
|
||||
<ShimmeringLoader></ShimmeringLoader>
|
||||
<ShimmeringLoader />
|
||||
) : (
|
||||
pipeline.config.publication_name
|
||||
)}
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
<RowMenu
|
||||
pipelineStatus={pipelineStatusData?.status}
|
||||
error={pipelineStatusError}
|
||||
isLoading={isPipelineStatusLoading}
|
||||
isError={isPipelineStatusError}
|
||||
onEnableClick={onEnableClick}
|
||||
onDisableClick={onDisableClick}
|
||||
onDeleteClick={() => setShowDeleteDestinationForm(true)}
|
||||
onEditClick={() => setShowEditDestinationPanel(true)}
|
||||
></RowMenu>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RowMenu
|
||||
pipeline={pipeline}
|
||||
pipelineStatus={pipelineStatusData?.status}
|
||||
error={pipelineStatusError}
|
||||
isLoading={isPipelineStatusLoading}
|
||||
isError={isPipelineStatusError}
|
||||
onDeleteClick={() => setShowDeleteDestinationForm(true)}
|
||||
onEditClick={() => setShowEditDestinationPanel(true)}
|
||||
/>
|
||||
</div>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)}
|
||||
@@ -211,5 +172,3 @@ const DestinationRow = ({
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default DestinationRow
|
||||
@@ -1,19 +1,26 @@
|
||||
import { noop } from 'lodash'
|
||||
import { Plus, Search } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { useReplicationDestinationsQuery } from 'data/replication/destinations-query'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { Button, cn } from 'ui'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns'
|
||||
import DestinationRow from './DestinationRow'
|
||||
import { useReplicationPipelinesQuery } from 'data/replication/pipelines-query'
|
||||
import { useState } from 'react'
|
||||
import NewDestinationPanel from './DestinationPanel'
|
||||
import { useReplicationSourcesQuery } from 'data/replication/sources-query'
|
||||
import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold'
|
||||
import { Button, cn, Input_Shadcn_ } from 'ui'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns'
|
||||
import NewDestinationPanel from './DestinationPanel'
|
||||
import { DestinationRow } from './DestinationRow'
|
||||
import { PIPELINE_ERROR_MESSAGES } from './Pipeline.utils'
|
||||
|
||||
const Destinations = () => {
|
||||
interface DestinationsProps {
|
||||
onSelectPipeline?: (pipelineId: number, destinationName: string) => void
|
||||
}
|
||||
|
||||
export const Destinations = ({ onSelectPipeline = noop }: DestinationsProps) => {
|
||||
const [showNewDestinationPanel, setShowNewDestinationPanel] = useState(false)
|
||||
const [filterString, setFilterString] = useState<string>('')
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
const {
|
||||
@@ -21,12 +28,11 @@ const Destinations = () => {
|
||||
error: sourcesError,
|
||||
isLoading: isSourcesLoading,
|
||||
isError: isSourcesError,
|
||||
isSuccess: isSourcesSuccess,
|
||||
} = useReplicationSourcesQuery({
|
||||
projectRef,
|
||||
})
|
||||
|
||||
let sourceId = sourcesData?.sources.find((s) => s.name === projectRef)?.id
|
||||
const sourceId = sourcesData?.sources.find((s) => s.name === projectRef)?.id
|
||||
|
||||
const {
|
||||
data: destinationsData,
|
||||
@@ -50,21 +56,44 @@ const Destinations = () => {
|
||||
|
||||
const anyDestinations = isDestinationsSuccess && destinationsData.destinations.length > 0
|
||||
|
||||
const filteredDestinations =
|
||||
filterString.length === 0
|
||||
? destinationsData?.destinations ?? []
|
||||
: (destinationsData?.destinations ?? []).filter((destination) =>
|
||||
destination.name.toLowerCase().includes(filterString.toLowerCase())
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScaffoldSection isFullWidth>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<ScaffoldSectionTitle>Destinations</ScaffoldSectionTitle>
|
||||
<div className="mb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<div className="relative">
|
||||
<Search
|
||||
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-foreground-lighter"
|
||||
size={14}
|
||||
/>
|
||||
<Input_Shadcn_
|
||||
className="pl-9 h-7"
|
||||
placeholder={'Filter destinations'}
|
||||
value={filterString}
|
||||
onChange={(e) => setFilterString(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="default" icon={<Plus />} onClick={() => setShowNewDestinationPanel(true)}>
|
||||
Add destination
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full overflow-hidden overflow-x-auto">
|
||||
{(isSourcesLoading || isDestinationsLoading) && <GenericSkeletonLoader />}
|
||||
|
||||
{(isSourcesError || isDestinationsError) && (
|
||||
<AlertError
|
||||
error={sourcesError || destinationsError}
|
||||
subject="Failed to retrieve destinations"
|
||||
subject={PIPELINE_ERROR_MESSAGES.RETRIEVE_DESTINATIONS}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -77,7 +106,7 @@ const Destinations = () => {
|
||||
<Table.th key="publication">Publication</Table.th>,
|
||||
<Table.th key="actions"></Table.th>,
|
||||
]}
|
||||
body={destinationsData.destinations.map((destination) => {
|
||||
body={filteredDestinations.map((destination) => {
|
||||
const pipeline = pipelinesData?.pipelines.find(
|
||||
(p) => p.destination_id === destination.id
|
||||
)
|
||||
@@ -93,7 +122,8 @@ const Destinations = () => {
|
||||
isLoading={isPipelinesLoading}
|
||||
isError={isPipelinesError}
|
||||
isSuccess={isPipelinesSuccess}
|
||||
></DestinationRow>
|
||||
onSelectPipeline={onSelectPipeline}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
></Table>
|
||||
@@ -124,15 +154,22 @@ const Destinations = () => {
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</ScaffoldSection>
|
||||
</div>
|
||||
|
||||
{!isSourcesLoading &&
|
||||
!isDestinationsLoading &&
|
||||
filteredDestinations.length === 0 &&
|
||||
anyDestinations && (
|
||||
<div className="text-center py-8 text-foreground-light">
|
||||
<p>No destinations match "{filterString}"</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<NewDestinationPanel
|
||||
visible={showNewDestinationPanel}
|
||||
sourceId={sourceId}
|
||||
onClose={() => setShowNewDestinationPanel(false)}
|
||||
></NewDestinationPanel>
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Destinations
|
||||
@@ -0,0 +1,96 @@
|
||||
import { ReplicationPipelineStatusData } from 'data/replication/pipeline-status-query'
|
||||
import { PipelineStatusRequestStatus } from 'state/replication-pipeline-request-status'
|
||||
|
||||
export const PIPELINE_ERROR_MESSAGES = {
|
||||
RETRIEVE_PIPELINE: 'Failed to retrieve pipeline information',
|
||||
RETRIEVE_PIPELINE_STATUS: 'Failed to retrieve pipeline status',
|
||||
RETRIEVE_REPLICATION_STATUS: 'Failed to retrieve table replication status',
|
||||
RETRIEVE_DESTINATIONS: 'Failed to retrieve destinations',
|
||||
ENABLE_DESTINATION: 'Failed to enable destination',
|
||||
DISABLE_DESTINATION: 'Failed to disable destination',
|
||||
DELETE_DESTINATION: 'Failed to delete destination',
|
||||
NO_PIPELINE_FOUND: 'No pipeline found',
|
||||
COPY_TABLE_STATUS: 'Failed to copy table status',
|
||||
} as const
|
||||
|
||||
export const getStatusName = (
|
||||
status: ReplicationPipelineStatusData['status'] | undefined
|
||||
): ReplicationPipelineStatusData['status']['name'] | undefined => {
|
||||
if (status && typeof status === 'object' && 'name' in status) {
|
||||
return status.name
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export const PIPELINE_STATE_MESSAGES = {
|
||||
enabling: {
|
||||
title: 'Pipeline Enabling',
|
||||
message: 'Starting the pipeline. Table replication will resume once enabled.',
|
||||
badge: 'Enabling',
|
||||
},
|
||||
disabling: {
|
||||
title: 'Pipeline Disabling',
|
||||
message: 'Stopping the pipeline. Table replication will be paused once disabled.',
|
||||
badge: 'Disabling',
|
||||
},
|
||||
failed: {
|
||||
title: 'Pipeline Failed',
|
||||
message: 'Replication has encountered an error. Check the logs for details.',
|
||||
badge: 'Failed',
|
||||
},
|
||||
stopped: {
|
||||
title: 'Pipeline Stopped',
|
||||
message: 'Replication is paused. Enable the pipeline to resume data synchronization.',
|
||||
badge: 'Stopped',
|
||||
},
|
||||
starting: {
|
||||
title: 'Pipeline Starting',
|
||||
message: 'Initializing replication. Table status will be available once running.',
|
||||
badge: 'Starting',
|
||||
},
|
||||
running: {
|
||||
title: 'Pipeline Running',
|
||||
message: 'Replication is active and processing data',
|
||||
badge: 'Running',
|
||||
},
|
||||
unknown: {
|
||||
title: 'Pipeline Status Unknown',
|
||||
message: 'Unable to determine replication status. Check the logs for more information.',
|
||||
badge: 'Unknown',
|
||||
},
|
||||
notRunning: {
|
||||
title: 'Pipeline Not Running',
|
||||
message: 'Replication is not active. Enable the pipeline to start data synchronization.',
|
||||
badge: 'Disabled',
|
||||
},
|
||||
} as const
|
||||
|
||||
export const getPipelineStateMessages = (
|
||||
requestStatus: PipelineStatusRequestStatus | undefined,
|
||||
statusName: string | undefined
|
||||
) => {
|
||||
// Always prioritize request status (enabling/disabling) over pipeline status
|
||||
if (requestStatus === PipelineStatusRequestStatus.EnableRequested) {
|
||||
return PIPELINE_STATE_MESSAGES.enabling
|
||||
}
|
||||
|
||||
if (requestStatus === PipelineStatusRequestStatus.DisableRequested) {
|
||||
return PIPELINE_STATE_MESSAGES.disabling
|
||||
}
|
||||
|
||||
// Only check pipeline status if no request is in progress
|
||||
switch (statusName) {
|
||||
case 'failed':
|
||||
return PIPELINE_STATE_MESSAGES.failed
|
||||
case 'stopped':
|
||||
return PIPELINE_STATE_MESSAGES.stopped
|
||||
case 'starting':
|
||||
return PIPELINE_STATE_MESSAGES.starting
|
||||
case 'started':
|
||||
return PIPELINE_STATE_MESSAGES.running
|
||||
case 'unknown':
|
||||
return PIPELINE_STATE_MESSAGES.unknown
|
||||
default:
|
||||
return PIPELINE_STATE_MESSAGES.notRunning
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,11 @@
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
|
||||
import { cn } from 'ui'
|
||||
import { ResponseError } from 'types'
|
||||
import { Loader2, ChevronDown, ChevronRight, Copy, AlertTriangle } from 'lucide-react'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
Button,
|
||||
Collapsible_Shadcn_ as Collapsible,
|
||||
CollapsibleContent_Shadcn_ as CollapsibleContent,
|
||||
CollapsibleTrigger_Shadcn_ as CollapsibleTrigger,
|
||||
} from 'ui'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { copyToClipboard } from 'ui'
|
||||
import { toast } from 'sonner'
|
||||
import { ReplicationPipelineStatusData } from 'data/replication/pipeline-status-query'
|
||||
|
||||
export enum PipelineStatusRequestStatus {
|
||||
None = 'None',
|
||||
EnableRequested = 'EnableRequested',
|
||||
DisableRequested = 'DisableRequested',
|
||||
}
|
||||
import { AlertTriangle, Loader2 } from 'lucide-react'
|
||||
import { PipelineStatusRequestStatus } from 'state/replication-pipeline-request-status'
|
||||
import { ResponseError } from 'types'
|
||||
import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
|
||||
import { getPipelineStateMessages, PIPELINE_ERROR_MESSAGES } from './Pipeline.utils'
|
||||
|
||||
export enum PipelineStatusName {
|
||||
FAILED = 'failed',
|
||||
@@ -41,10 +24,10 @@ interface PipelineStatusProps {
|
||||
isLoading: boolean
|
||||
isError: boolean
|
||||
isSuccess: boolean
|
||||
requestStatus: PipelineStatusRequestStatus
|
||||
requestStatus?: PipelineStatusRequestStatus
|
||||
}
|
||||
|
||||
const PipelineStatus = ({
|
||||
export const PipelineStatus = ({
|
||||
pipelineStatus,
|
||||
error,
|
||||
isLoading,
|
||||
@@ -52,34 +35,6 @@ const PipelineStatus = ({
|
||||
isSuccess,
|
||||
requestStatus,
|
||||
}: PipelineStatusProps) => {
|
||||
const [isErrorDetailsOpen, setIsErrorDetailsOpen] = useState(false)
|
||||
const errorDetailsRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (errorDetailsRef.current && !errorDetailsRef.current.contains(event.target as Node)) {
|
||||
setIsErrorDetailsOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (isErrorDetailsOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
}
|
||||
}, [isErrorDetailsOpen])
|
||||
|
||||
const handleCopyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await copyToClipboard(text)
|
||||
toast.success('Error details copied to clipboard')
|
||||
} catch (error) {
|
||||
toast.error('Failed to copy error details')
|
||||
}
|
||||
}
|
||||
|
||||
const isFailedStatus = (
|
||||
status: ReplicationPipelineStatusData['status'] | undefined
|
||||
): status is FailedStatus => {
|
||||
@@ -91,144 +46,37 @@ const PipelineStatus = ({
|
||||
)
|
||||
}
|
||||
|
||||
const isLogLoadingState = (failedStatus: FailedStatus): boolean => {
|
||||
// Right now we hardcode the error message which is returned when k8s is not able to find logs, which seems
|
||||
// to be a transient error. In case we find a way to properly handle this in the backend, this hack will not
|
||||
// be needed anymore.
|
||||
const renderFailedStatus = () => {
|
||||
return (
|
||||
failedStatus.message?.startsWith('unable to retrieve container logs for containerd://') ===
|
||||
true
|
||||
)
|
||||
}
|
||||
|
||||
const renderFailedStatus = (failedStatus: FailedStatus) => {
|
||||
const hasDetails =
|
||||
failedStatus.message || failedStatus.reason || failedStatus.exit_code !== undefined
|
||||
const isLoadingLogs = isLogLoadingState(failedStatus)
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-2 text-sm text-destructive">
|
||||
{isLoadingLogs ? (
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
) : (
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
)}
|
||||
<span>Failed</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{isLoadingLogs
|
||||
? 'Pipeline failed - logs are being retrieved from container'
|
||||
: 'Pipeline has failed - expand for error details'}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
{hasDetails && (
|
||||
<Collapsible open={isErrorDetailsOpen} onOpenChange={setIsErrorDetailsOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button type="text" size="tiny" className="h-auto py-0 px-1 text-xs">
|
||||
{isErrorDetailsOpen ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="absolute z-10 mt-1 left-0 min-w-[400px] max-w-[600px]">
|
||||
<div
|
||||
ref={errorDetailsRef}
|
||||
className="rounded-lg border border-border bg-surface-100 p-3 shadow-md"
|
||||
>
|
||||
{isLoadingLogs ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<Loader2 className="w-4 h-4 animate-spin text-warning-600" />
|
||||
<div>
|
||||
<div className="text-sm font-medium text-foreground mb-1">
|
||||
Pipeline Failed - Loading logs...
|
||||
</div>
|
||||
<div className="text-xs text-foreground-light">
|
||||
Error logs are being retrieved from the container. This may take a few
|
||||
moments.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
Pipeline Error Details
|
||||
</span>
|
||||
{(failedStatus.message || failedStatus.reason) && (
|
||||
<Button
|
||||
type="text"
|
||||
size="tiny"
|
||||
onClick={async () => {
|
||||
await handleCopyToClipboard(
|
||||
[
|
||||
failedStatus.exit_code !== undefined
|
||||
? `Exit Code: ${failedStatus.exit_code}`
|
||||
: null,
|
||||
failedStatus.reason ? `Reason: ${failedStatus.reason}` : null,
|
||||
failedStatus.message ? `Message: ${failedStatus.message}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
)
|
||||
}}
|
||||
>
|
||||
<Copy className="w-3 h-3" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{failedStatus.exit_code !== undefined && (
|
||||
<div>
|
||||
<span className="text-xs font-medium text-foreground-light">
|
||||
Exit Code:
|
||||
</span>
|
||||
<div className="font-mono text-sm text-foreground mt-1">
|
||||
{failedStatus.exit_code}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{failedStatus.reason && (
|
||||
<div>
|
||||
<span className="text-xs font-medium text-foreground-light">Reason:</span>
|
||||
<pre className="font-mono text-sm text-foreground whitespace-pre-wrap break-words mt-1 max-h-32 overflow-y-auto">
|
||||
{failedStatus.reason}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{failedStatus.message && (
|
||||
<div>
|
||||
<span className="text-xs font-medium text-foreground-light">
|
||||
Message:
|
||||
</span>
|
||||
<pre className="font-mono text-sm text-foreground whitespace-pre-wrap break-words mt-1 max-h-32 overflow-y-auto">
|
||||
{failedStatus.message}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
)}
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-2 text-sm text-destructive">
|
||||
<AlertTriangle className="w-3 h-3" />
|
||||
<span>Failed</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{getPipelineStateMessages(requestStatus, 'failed').message}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
// Map backend statuses to UX-friendly display
|
||||
const getStatusConfig = () => {
|
||||
const statusName =
|
||||
pipelineStatus && typeof pipelineStatus === 'object' && 'name' in pipelineStatus
|
||||
? pipelineStatus.name
|
||||
: undefined
|
||||
|
||||
// Get consistent tooltip message using the same logic as other components
|
||||
const stateMessages = getPipelineStateMessages(requestStatus, statusName)
|
||||
|
||||
if (requestStatus === PipelineStatusRequestStatus.EnableRequested) {
|
||||
return {
|
||||
label: 'Enabling...',
|
||||
dot: <Loader2 className="animate-spin w-3 h-3 text-brand-600" />,
|
||||
color: 'text-brand-600',
|
||||
tooltip: 'Pipeline is being enabled and will start shortly',
|
||||
tooltip: stateMessages.message,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,7 +85,7 @@ const PipelineStatus = ({
|
||||
label: 'Disabling...',
|
||||
dot: <Loader2 className="animate-spin w-3 h-3 text-warning-600" />,
|
||||
color: 'text-warning-600',
|
||||
tooltip: 'Pipeline is being disabled and will stop shortly',
|
||||
tooltip: stateMessages.message,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,35 +103,35 @@ const PipelineStatus = ({
|
||||
label: 'Starting',
|
||||
dot: <Loader2 className="animate-spin w-3 h-3 text-warning-600" />,
|
||||
color: 'text-warning-600',
|
||||
tooltip: 'Pipeline is initializing and will be ready soon',
|
||||
tooltip: stateMessages.message,
|
||||
}
|
||||
case PipelineStatusName.STARTED:
|
||||
return {
|
||||
label: 'Running',
|
||||
dot: <div className="w-2 h-2 bg-brand-600 rounded-full" />,
|
||||
color: 'text-brand-600',
|
||||
tooltip: 'Pipeline is active and processing data',
|
||||
tooltip: stateMessages.message,
|
||||
}
|
||||
case PipelineStatusName.STOPPED:
|
||||
return {
|
||||
label: 'Stopped',
|
||||
dot: <div className="w-2 h-2 bg-foreground-lighter rounded-full" />,
|
||||
color: 'text-foreground-light',
|
||||
tooltip: 'Pipeline is not running - enable to start processing',
|
||||
tooltip: stateMessages.message,
|
||||
}
|
||||
case PipelineStatusName.UNKNOWN:
|
||||
return {
|
||||
label: 'Unknown',
|
||||
dot: <div className="w-2 h-2 bg-warning-600 rounded-full" />,
|
||||
color: 'text-warning-600',
|
||||
tooltip: 'Pipeline status could not be determined',
|
||||
tooltip: stateMessages.message,
|
||||
}
|
||||
default:
|
||||
return {
|
||||
label: 'Unknown',
|
||||
dot: <div className="w-2 h-2 bg-destructive-600 rounded-full" />,
|
||||
color: 'text-destructive-600',
|
||||
tooltip: 'Pipeline status is unclear - check logs for details',
|
||||
tooltip: stateMessages.message,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,31 +149,27 @@ const PipelineStatus = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && <ShimmeringLoader></ShimmeringLoader>}
|
||||
{isError && <AlertError error={error} subject="Failed to retrieve pipeline status" />}
|
||||
{isLoading && <ShimmeringLoader />}
|
||||
{isError && (
|
||||
<AlertError error={error} subject={PIPELINE_ERROR_MESSAGES.RETRIEVE_PIPELINE_STATUS} />
|
||||
)}
|
||||
{isSuccess && (
|
||||
<>
|
||||
{statusConfig.isFailedStatus ? (
|
||||
<div className="relative">{renderFailedStatus(pipelineStatus as FailedStatus)}</div>
|
||||
<div className="relative">{renderFailedStatus()}</div>
|
||||
) : (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className={cn('flex items-center gap-2 text-sm', statusConfig.color)}>
|
||||
{statusConfig.dot}
|
||||
<span>{statusConfig.label}</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{statusConfig.tooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className={cn('flex items-center gap-2 text-sm w-min', statusConfig.color)}>
|
||||
{statusConfig.dot}
|
||||
<span>{statusConfig.label}</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{statusConfig.tooltip}</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PipelineStatus
|
||||
@@ -0,0 +1,396 @@
|
||||
import { Activity, AlertTriangle, ChevronLeft, Copy, ExternalLink, Search } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import Table from 'components/to-be-cleaned/Table'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
import { useReplicationPipelineByIdQuery } from 'data/replication/pipeline-by-id-query'
|
||||
import { useReplicationPipelineReplicationStatusQuery } from 'data/replication/pipeline-replication-status-query'
|
||||
import { useReplicationPipelineStatusQuery } from 'data/replication/pipeline-status-query'
|
||||
import { useStartPipelineMutation } from 'data/replication/start-pipeline-mutation'
|
||||
import { useStopPipelineMutation } from 'data/replication/stop-pipeline-mutation'
|
||||
import {
|
||||
PipelineStatusRequestStatus,
|
||||
usePipelineRequestStatus,
|
||||
} from 'state/replication-pipeline-request-status'
|
||||
import { Badge, Button, cn, copyToClipboard, Input_Shadcn_ } from 'ui'
|
||||
import { GenericSkeletonLoader } from 'ui-patterns'
|
||||
import { getStatusName, PIPELINE_ERROR_MESSAGES } from './Pipeline.utils'
|
||||
import { PipelineStatus } from './PipelineStatus'
|
||||
import { TableState } from './ReplicationPipelineStatus.types'
|
||||
import { getDisabledStateConfig, getStatusConfig } from './ReplicationPipelineStatus.utils'
|
||||
|
||||
interface ReplicationPipelineStatusProps {
|
||||
pipelineId: number
|
||||
destinationName?: string
|
||||
onSelectBack: () => void
|
||||
}
|
||||
|
||||
export const ReplicationPipelineStatus = ({
|
||||
pipelineId,
|
||||
destinationName,
|
||||
onSelectBack,
|
||||
}: ReplicationPipelineStatusProps) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
const [filterString, setFilterString] = useState<string>('')
|
||||
|
||||
const { getRequestStatus, updatePipelineStatus, setRequestStatus } = usePipelineRequestStatus()
|
||||
const requestStatus = getRequestStatus(pipelineId)
|
||||
|
||||
const {
|
||||
data: pipeline,
|
||||
error: pipelineError,
|
||||
isLoading: isPipelineLoading,
|
||||
isError: isPipelineError,
|
||||
} = useReplicationPipelineByIdQuery({
|
||||
projectRef,
|
||||
pipelineId,
|
||||
})
|
||||
|
||||
const {
|
||||
data: pipelineStatusData,
|
||||
error: pipelineStatusError,
|
||||
isLoading: isPipelineStatusLoading,
|
||||
isError: isPipelineStatusError,
|
||||
isSuccess: isPipelineStatusSuccess,
|
||||
} = useReplicationPipelineStatusQuery(
|
||||
{ projectRef, pipelineId },
|
||||
{
|
||||
enabled: !!pipelineId,
|
||||
refetchInterval: 2000, // Poll every 2 seconds
|
||||
}
|
||||
)
|
||||
|
||||
const {
|
||||
data: replicationStatusData,
|
||||
error: statusError,
|
||||
isLoading: isStatusLoading,
|
||||
isError: isStatusError,
|
||||
} = useReplicationPipelineReplicationStatusQuery(
|
||||
{ projectRef, pipelineId },
|
||||
{
|
||||
enabled: !!pipelineId,
|
||||
refetchInterval: 2000, // Poll every 2 seconds
|
||||
}
|
||||
)
|
||||
|
||||
const { mutateAsync: startPipeline, isLoading: isStartingPipeline } = useStartPipelineMutation()
|
||||
const { mutateAsync: stopPipeline, isLoading: isStoppingPipeline } = useStopPipelineMutation()
|
||||
|
||||
const statusName = getStatusName(pipelineStatusData?.status)
|
||||
const config = getDisabledStateConfig({ requestStatus, statusName })
|
||||
|
||||
const tableStatuses = replicationStatusData?.table_statuses || []
|
||||
const filteredTableStatuses =
|
||||
filterString.length === 0
|
||||
? tableStatuses
|
||||
: tableStatuses.filter((table: TableState) =>
|
||||
table.table_name.toLowerCase().includes(filterString.toLowerCase())
|
||||
)
|
||||
|
||||
const errorTables = tableStatuses.filter((table: TableState) => table.state.name === 'error')
|
||||
const hasErrors = errorTables.length > 0
|
||||
const isPipelineRunning = statusName === 'started'
|
||||
const hasTableData = tableStatuses.length > 0
|
||||
const isEnablingDisabling =
|
||||
requestStatus === PipelineStatusRequestStatus.EnableRequested ||
|
||||
requestStatus === PipelineStatusRequestStatus.DisableRequested
|
||||
const showDisabledState = !isPipelineRunning || isEnablingDisabling
|
||||
|
||||
const handleCopyTableStatus = async (tableName: string, state: TableState['state']) => {
|
||||
const statusText = `Table: ${tableName}\nStatus: ${state.name}${
|
||||
'message' in state ? `\nError: ${state.message}` : ''
|
||||
}${'lag' in state ? `\nLag: ${state.lag}ms` : ''}`
|
||||
|
||||
try {
|
||||
await copyToClipboard(statusText)
|
||||
toast.success('Table status copied to clipboard')
|
||||
} catch {
|
||||
toast.error(PIPELINE_ERROR_MESSAGES.COPY_TABLE_STATUS)
|
||||
}
|
||||
}
|
||||
|
||||
const onTogglePipeline = async () => {
|
||||
if (!projectRef) {
|
||||
return console.error('Project ref is required')
|
||||
}
|
||||
if (!pipeline) {
|
||||
return toast.error(PIPELINE_ERROR_MESSAGES.NO_PIPELINE_FOUND)
|
||||
}
|
||||
|
||||
try {
|
||||
if (statusName === 'stopped') {
|
||||
await startPipeline({ projectRef, pipelineId: pipeline.id })
|
||||
setRequestStatus(pipeline.id, PipelineStatusRequestStatus.EnableRequested)
|
||||
} else if (statusName === 'started') {
|
||||
await stopPipeline({ projectRef, pipelineId: pipeline.id })
|
||||
setRequestStatus(pipeline.id, PipelineStatusRequestStatus.DisableRequested)
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error(PIPELINE_ERROR_MESSAGES.ENABLE_DESTINATION)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updatePipelineStatus(pipelineId, statusName)
|
||||
}, [pipelineId, statusName, updatePipelineStatus])
|
||||
|
||||
if (isPipelineError) {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-x-3">
|
||||
<Button
|
||||
type="outline"
|
||||
onClick={onSelectBack}
|
||||
icon={<ChevronLeft />}
|
||||
style={{ padding: '5px' }}
|
||||
/>
|
||||
<h3 className="text-xl font-semibold">Pipeline Status</h3>
|
||||
</div>
|
||||
</div>
|
||||
<AlertError error={pipelineError} subject={PIPELINE_ERROR_MESSAGES.RETRIEVE_PIPELINE} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Header with back button and filters */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-x-3">
|
||||
<Button
|
||||
type="outline"
|
||||
onClick={onSelectBack}
|
||||
icon={<ChevronLeft />}
|
||||
style={{ padding: '5px' }}
|
||||
/>
|
||||
<div>
|
||||
<div className="flex items-center gap-x-3">
|
||||
<h3 className="text-xl font-semibold">{destinationName || 'Pipeline'}</h3>
|
||||
<PipelineStatus
|
||||
pipelineStatus={pipelineStatusData?.status}
|
||||
error={pipelineStatusError}
|
||||
isLoading={isPipelineStatusLoading}
|
||||
isError={isPipelineStatusError}
|
||||
isSuccess={isPipelineStatusSuccess}
|
||||
requestStatus={requestStatus}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className="relative">
|
||||
<Search
|
||||
className="absolute left-2 top-1/2 transform -translate-y-1/2 text-foreground-lighter"
|
||||
size={14}
|
||||
/>
|
||||
<Input_Shadcn_
|
||||
className="pl-7 h-[26px] text-xs"
|
||||
placeholder="Search for tables"
|
||||
value={filterString}
|
||||
onChange={(e) => setFilterString(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type={statusName === 'stopped' ? 'primary' : 'default'}
|
||||
onClick={() => onTogglePipeline()}
|
||||
loading={isStartingPipeline || isStoppingPipeline}
|
||||
disabled={!['failed', 'started', 'stopped'].includes(statusName ?? '')}
|
||||
>
|
||||
{statusName === 'stopped' ? 'Enable' : 'Disable'} pipeline
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(isPipelineLoading || isStatusLoading) && <GenericSkeletonLoader />}
|
||||
|
||||
{isStatusError && (
|
||||
<AlertError
|
||||
error={statusError}
|
||||
subject={PIPELINE_ERROR_MESSAGES.RETRIEVE_REPLICATION_STATUS}
|
||||
/>
|
||||
)}
|
||||
|
||||
{hasErrors && (
|
||||
<div className="p-4 border border-destructive-300 bg-destructive-100 rounded-lg">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertTriangle className="w-5 h-5 text-destructive-600 mt-0.5 shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-medium text-destructive-900 mb-1">
|
||||
{errorTables.length} table{errorTables.length > 1 ? 's' : ''} failed
|
||||
</h4>
|
||||
<p className="text-sm text-destructive-700 mb-3">
|
||||
Some tables encountered replication errors. Check the logs for detailed error
|
||||
information.
|
||||
</p>
|
||||
<Button
|
||||
asChild
|
||||
type="outline"
|
||||
size="tiny"
|
||||
icon={<ExternalLink className="w-3 h-3" />}
|
||||
className="text-destructive-600 border-destructive-300 hover:bg-destructive-50"
|
||||
>
|
||||
<Link
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
href={`/project/${projectRef}/logs/postgres-logs`}
|
||||
>
|
||||
View Logs
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasTableData && (
|
||||
<div className="flex flex-col gap-y-4">
|
||||
{showDisabledState && (
|
||||
<div
|
||||
className={cn(
|
||||
'p-4 border border-default rounded-lg flex items-center justify-between',
|
||||
config.colors.bg
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-x-3">
|
||||
<div
|
||||
className={cn(
|
||||
'w-10 h-10 rounded-full flex items-center justify-center',
|
||||
config.colors.iconBg
|
||||
)}
|
||||
>
|
||||
<div className={config.colors.icon}>{config.icon}</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h4 className={`text-sm font-medium ${config.colors.text}`}>{config.title}</h4>
|
||||
<p className={`text-sm ${config.colors.subtext}`}>{config.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="w-full overflow-hidden overflow-x-auto">
|
||||
<Table
|
||||
head={[
|
||||
<Table.th key="table">Table</Table.th>,
|
||||
<Table.th key="status">Status</Table.th>,
|
||||
<Table.th key="details">Details</Table.th>,
|
||||
<Table.th key="actions" className="w-16 text-center">
|
||||
Actions
|
||||
</Table.th>,
|
||||
]}
|
||||
body={filteredTableStatuses.map((table: TableState, index: number) => {
|
||||
const statusConfig = getStatusConfig(table.state)
|
||||
return (
|
||||
<Table.tr key={`${table.table_name}-${index}`} className="border-t">
|
||||
<Table.td>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<p>{table.table_name}</p>
|
||||
|
||||
<ButtonTooltip
|
||||
asChild
|
||||
type="text"
|
||||
className="px-1.5"
|
||||
icon={<ExternalLink />}
|
||||
tooltip={{ content: { side: 'bottom', text: 'Open in Table Editor' } }}
|
||||
>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={`/project/${projectRef}/editor/${table.table_id}`}
|
||||
/>
|
||||
</ButtonTooltip>
|
||||
</div>
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
{showDisabledState ? (
|
||||
<Badge variant="default">Not Available</Badge>
|
||||
) : (
|
||||
statusConfig.badge
|
||||
)}
|
||||
</Table.td>
|
||||
<Table.td>
|
||||
{showDisabledState ? (
|
||||
<p className="text-sm text-foreground-lighter">
|
||||
Status unavailable while pipeline is {config.badge.toLowerCase()}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm text-foreground">{statusConfig.description}</div>
|
||||
{'lag' in table.state && (
|
||||
<div className="text-xs text-foreground-light">
|
||||
Lag: {table.state.lag}ms
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Table.td>
|
||||
<Table.td className="text-center">
|
||||
<ButtonTooltip
|
||||
type="text"
|
||||
size="tiny"
|
||||
icon={<Copy className="w-3 h-3" />}
|
||||
className="px-1.5"
|
||||
disabled={showDisabledState}
|
||||
onClick={() => handleCopyTableStatus(table.table_name, table.state)}
|
||||
tooltip={{
|
||||
content: {
|
||||
side: 'bottom',
|
||||
text: showDisabledState
|
||||
? `Copy unavailable while pipeline is ${config.badge.toLowerCase()}`
|
||||
: 'Copy status details',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredTableStatuses.length === 0 && hasTableData && (
|
||||
<Table.tr>
|
||||
<Table.td colSpan={4}>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-foreground">No results found</p>
|
||||
<p className="text-sm text-foreground-light">
|
||||
Your search for "{filterString}" did not return any results
|
||||
</p>
|
||||
</div>
|
||||
</Table.td>
|
||||
</Table.tr>
|
||||
)}
|
||||
|
||||
{!isStatusLoading && tableStatuses.length === 0 && (
|
||||
<div className="flex flex-col items-center justify-center py-16 px-4">
|
||||
<div className="w-full max-w-sm mx-auto text-center space-y-4">
|
||||
<div className="w-16 h-16 bg-surface-200 rounded-full flex items-center justify-center mx-auto">
|
||||
<Activity className="w-8 h-8 text-foreground-lighter" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-lg font-semibold text-foreground">
|
||||
{showDisabledState ? 'Pipeline Not Running' : 'No table status information'}
|
||||
</h4>
|
||||
<p className="text-sm text-foreground-light leading-relaxed">
|
||||
{showDisabledState
|
||||
? `The replication pipeline is currently ${statusName || 'not active'}. Table status
|
||||
information is not available while the pipeline is in this state.`
|
||||
: `This pipeline doesn't have any table replication status data available yet. The status will appear here once replication begins.`}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-foreground-lighter">
|
||||
Data refreshes automatically every 2 seconds
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
export type TableState = {
|
||||
table_id: number
|
||||
table_name: string
|
||||
state:
|
||||
| { name: 'queued' }
|
||||
| { name: 'copying_table' }
|
||||
| { name: 'copied_table' }
|
||||
| { name: 'following_wal'; lag: number }
|
||||
| { name: 'error'; message: string }
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import { ReplicationPipelineStatusData } from 'data/replication/pipeline-status-query'
|
||||
import { Activity, Clock, HelpCircle, Loader2, XCircle } from 'lucide-react'
|
||||
import { PipelineStatusRequestStatus } from 'state/replication-pipeline-request-status'
|
||||
import { Badge } from 'ui'
|
||||
import { getPipelineStateMessages } from './Pipeline.utils'
|
||||
import { TableState } from './ReplicationPipelineStatus.types'
|
||||
|
||||
export const getStatusConfig = (state: TableState['state']) => {
|
||||
switch (state.name) {
|
||||
case 'queued':
|
||||
return {
|
||||
badge: <Badge variant="warning">Queued</Badge>,
|
||||
description: 'Waiting to start replication',
|
||||
color: 'text-warning-600',
|
||||
}
|
||||
case 'copying_table':
|
||||
return {
|
||||
badge: <Badge variant="brand">Copying</Badge>,
|
||||
description: 'Initial data copy in progress',
|
||||
color: 'text-brand-600',
|
||||
}
|
||||
case 'copied_table':
|
||||
return {
|
||||
badge: <Badge variant="success">Copied</Badge>,
|
||||
description: 'Initial copy completed',
|
||||
color: 'text-success-600',
|
||||
}
|
||||
case 'following_wal':
|
||||
return {
|
||||
badge: <Badge variant="success">Live</Badge>,
|
||||
description: `Replicating live changes`,
|
||||
color: 'text-success-600',
|
||||
}
|
||||
case 'error':
|
||||
return {
|
||||
badge: <Badge variant="destructive">Error</Badge>,
|
||||
description: state.message,
|
||||
color: 'text-destructive-600',
|
||||
}
|
||||
default:
|
||||
return {
|
||||
badge: <Badge variant="warning">Unknown</Badge>,
|
||||
description: 'Unknown status',
|
||||
color: 'text-warning-600',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const getDisabledStateConfig = ({
|
||||
requestStatus,
|
||||
statusName,
|
||||
}: {
|
||||
requestStatus: PipelineStatusRequestStatus
|
||||
statusName?: ReplicationPipelineStatusData['status']['name']
|
||||
}) => {
|
||||
const { title, message, badge } = getPipelineStateMessages(requestStatus, statusName)
|
||||
|
||||
// Get icon and colors based on current state
|
||||
const isEnabling = requestStatus === PipelineStatusRequestStatus.EnableRequested
|
||||
const isDisabling = requestStatus === PipelineStatusRequestStatus.DisableRequested
|
||||
const isTransitioning = isEnabling || isDisabling
|
||||
|
||||
const icon = isTransitioning ? (
|
||||
<Loader2 className="w-6 h-6 animate-spin" />
|
||||
) : statusName === 'failed' ? (
|
||||
<XCircle className="w-6 h-6" />
|
||||
) : statusName === 'starting' ? (
|
||||
<Clock className="w-6 h-6" />
|
||||
) : statusName === 'unknown' ? (
|
||||
<HelpCircle className="w-6 h-6" />
|
||||
) : (
|
||||
<Activity className="w-6 h-6" />
|
||||
)
|
||||
|
||||
const colors = isEnabling
|
||||
? {
|
||||
bg: 'bg-brand-50',
|
||||
text: 'text-brand-900',
|
||||
subtext: 'text-brand-700',
|
||||
iconBg: 'bg-brand-600',
|
||||
icon: 'text-white dark:text-black',
|
||||
}
|
||||
: isDisabling || statusName === 'starting' || statusName === 'unknown'
|
||||
? {
|
||||
bg: 'bg-warning-50',
|
||||
text: 'text-warning-900',
|
||||
subtext: 'text-warning-700',
|
||||
iconBg: 'bg-warning-600',
|
||||
icon: 'text-white dark:text-black',
|
||||
}
|
||||
: statusName === 'failed'
|
||||
? {
|
||||
bg: 'bg-destructive-50',
|
||||
text: 'text-destructive-900',
|
||||
subtext: 'text-destructive-700',
|
||||
iconBg: 'bg-destructive-600',
|
||||
icon: 'text-white dark:text-black',
|
||||
}
|
||||
: {
|
||||
bg: 'bg-surface-100',
|
||||
text: 'text-foreground',
|
||||
subtext: 'text-foreground-light',
|
||||
iconBg: 'bg-foreground-lighter',
|
||||
icon: 'text-white dark:text-black',
|
||||
}
|
||||
|
||||
return { title, message, badge, icon, colors }
|
||||
}
|
||||
@@ -1,5 +1,14 @@
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { Edit, MoreVertical, Pause, Play, Trash } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import AlertError from 'components/ui/AlertError'
|
||||
import { useStartPipelineMutation } from 'data/replication/start-pipeline-mutation'
|
||||
import { useStopPipelineMutation } from 'data/replication/stop-pipeline-mutation'
|
||||
import {
|
||||
PipelineStatusRequestStatus,
|
||||
usePipelineRequestStatus,
|
||||
} from 'state/replication-pipeline-request-status'
|
||||
import { ResponseError } from 'types'
|
||||
import {
|
||||
Button,
|
||||
@@ -10,29 +19,31 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from 'ui'
|
||||
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
|
||||
import { Pipeline } from './DestinationRow'
|
||||
import { PIPELINE_ERROR_MESSAGES } from './Pipeline.utils'
|
||||
import { PipelineStatusName } from './PipelineStatus'
|
||||
|
||||
interface RowMenuProps {
|
||||
pipeline: Pipeline | undefined
|
||||
pipelineStatus: any
|
||||
error: ResponseError | null
|
||||
isLoading: boolean
|
||||
isError: boolean
|
||||
onEnableClick: () => void
|
||||
onDisableClick: () => void
|
||||
onEditClick: () => void
|
||||
onDeleteClick: () => void
|
||||
}
|
||||
|
||||
const RowMenu = ({
|
||||
export const RowMenu = ({
|
||||
pipeline,
|
||||
pipelineStatus,
|
||||
error,
|
||||
isLoading,
|
||||
isError,
|
||||
onEnableClick,
|
||||
onDisableClick,
|
||||
onEditClick,
|
||||
onDeleteClick,
|
||||
}: RowMenuProps) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
const getStatusName = (status: any) => {
|
||||
if (status && typeof status === 'object' && 'name' in status) {
|
||||
return status.name
|
||||
@@ -43,33 +54,104 @@ const RowMenu = ({
|
||||
const statusName = getStatusName(pipelineStatus)
|
||||
const pipelineEnabled = statusName !== PipelineStatusName.STOPPED
|
||||
|
||||
const { mutateAsync: startPipeline } = useStartPipelineMutation()
|
||||
const { mutateAsync: stopPipeline } = useStopPipelineMutation()
|
||||
const { setRequestStatus: setGlobalRequestStatus } = usePipelineRequestStatus()
|
||||
|
||||
const onEnablePipeline = async () => {
|
||||
if (!projectRef) {
|
||||
return console.error('Project ref is required')
|
||||
}
|
||||
if (!pipeline) {
|
||||
return toast.error(PIPELINE_ERROR_MESSAGES.NO_PIPELINE_FOUND)
|
||||
}
|
||||
|
||||
try {
|
||||
await startPipeline({ projectRef, pipelineId: pipeline.id })
|
||||
toast(`Enabling pipeline ${pipeline.destination_name}`)
|
||||
setGlobalRequestStatus(pipeline.id, PipelineStatusRequestStatus.EnableRequested)
|
||||
} catch (error) {
|
||||
toast.error(PIPELINE_ERROR_MESSAGES.ENABLE_DESTINATION)
|
||||
}
|
||||
}
|
||||
|
||||
const onDisablePipeline = async () => {
|
||||
if (!projectRef) {
|
||||
console.error('Project ref is required')
|
||||
return
|
||||
}
|
||||
if (!pipeline) {
|
||||
toast.error(PIPELINE_ERROR_MESSAGES.NO_PIPELINE_FOUND)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await stopPipeline({ projectRef, pipelineId: pipeline.id })
|
||||
toast(`Disabling pipeline ${pipeline.destination_name}`)
|
||||
setGlobalRequestStatus(pipeline.id, PipelineStatusRequestStatus.DisableRequested)
|
||||
} catch (error) {
|
||||
toast.error(PIPELINE_ERROR_MESSAGES.DISABLE_DESTINATION)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-end items-center space-x-2">
|
||||
{isLoading && <ShimmeringLoader></ShimmeringLoader>}
|
||||
{isError && <AlertError error={error} subject="Failed to retrieve pipeline status" />}
|
||||
{isLoading && <ShimmeringLoader />}
|
||||
{isError && (
|
||||
<AlertError error={error} subject={PIPELINE_ERROR_MESSAGES.RETRIEVE_PIPELINE_STATUS} />
|
||||
)}
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="default" className="px-1.5" icon={<MoreVertical />} />
|
||||
<Button
|
||||
type="default"
|
||||
className="px-1.5"
|
||||
icon={<MoreVertical />}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="bottom" align="end" className="w-52">
|
||||
{pipelineEnabled ? (
|
||||
<DropdownMenuItem className="space-x-2" onClick={onDisableClick}>
|
||||
<DropdownMenuItem
|
||||
className="space-x-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDisablePipeline()
|
||||
}}
|
||||
>
|
||||
<Pause size={14} />
|
||||
<p>Disable</p>
|
||||
<p>Disable pipeline</p>
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem className="space-x-2" onClick={onEnableClick}>
|
||||
<DropdownMenuItem
|
||||
className="space-x-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onEnablePipeline()
|
||||
}}
|
||||
>
|
||||
<Play size={14} />
|
||||
<p>Enable</p>
|
||||
<p>Enable pipeline</p>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="space-x-2" onClick={onEditClick}>
|
||||
<DropdownMenuItem
|
||||
className="space-x-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onEditClick()
|
||||
}}
|
||||
>
|
||||
<Edit size={14} />
|
||||
<p>Edit destination</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="space-x-2" onClick={onDeleteClick}>
|
||||
<DropdownMenuItem
|
||||
className="space-x-2"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDeleteClick()
|
||||
}}
|
||||
>
|
||||
<Trash stroke="red" size={14} />
|
||||
<p>Delete destination</p>
|
||||
</DropdownMenuItem>
|
||||
@@ -78,5 +160,3 @@ const RowMenu = ({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RowMenu
|
||||
+18
-21
@@ -15,7 +15,6 @@ import {
|
||||
DrawerTrigger,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from 'ui'
|
||||
import { useMediaQuery } from '../hooks/useMediaQuery'
|
||||
@@ -32,26 +31,24 @@ export function DataTableFilterControlsDrawer() {
|
||||
|
||||
return (
|
||||
<Drawer>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DrawerTrigger asChild>
|
||||
<Button className="h-9 w-9" ref={isMobile ? triggerButtonRef : null}>
|
||||
<FilterIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>
|
||||
Toggle controls with{' '}
|
||||
<Kbd className="ml-1 text-muted-foreground group-hover:text-accent-foreground">
|
||||
<span className="mr-1">⌘</span>
|
||||
<span>B</span>
|
||||
</Kbd>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DrawerTrigger asChild>
|
||||
<Button className="h-9 w-9" ref={isMobile ? triggerButtonRef : null}>
|
||||
<FilterIcon className="w-4 h-4" />
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>
|
||||
Toggle controls with{' '}
|
||||
<Kbd className="ml-1 text-muted-foreground group-hover:text-accent-foreground">
|
||||
<span className="mr-1">⌘</span>
|
||||
<span>B</span>
|
||||
</Kbd>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DrawerContent className="max-h-[calc(100dvh-4rem)]">
|
||||
<VisuallyHidden>
|
||||
<DrawerHeader>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
import { useHotKey } from 'hooks/ui/useHotKey'
|
||||
import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from 'ui'
|
||||
import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
import { Kbd } from './primitives/Kbd'
|
||||
import { useDataTable } from './providers/DataTableProvider'
|
||||
|
||||
@@ -10,28 +10,21 @@ export function DataTableResetButton() {
|
||||
useHotKey(table.resetColumnFilters, 'Escape')
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="default"
|
||||
size="tiny"
|
||||
onClick={() => table.resetColumnFilters()}
|
||||
icon={<X />}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
<p>
|
||||
Reset filters with{' '}
|
||||
<Kbd className="ml-1 text-muted-foreground group-hover:text-accent-foreground">
|
||||
<span className="mr-1">⌘</span>
|
||||
<span>Esc</span>
|
||||
</Kbd>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button type="default" size="tiny" onClick={() => table.resetColumnFilters()} icon={<X />}>
|
||||
Reset
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
<p>
|
||||
Reset filters with{' '}
|
||||
<Kbd className="ml-1 text-muted-foreground group-hover:text-accent-foreground">
|
||||
<span className="mr-1">⌘</span>
|
||||
<span>Esc</span>
|
||||
</Kbd>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +1,7 @@
|
||||
import { ChevronDown, ChevronUp, X } from 'lucide-react'
|
||||
import { ReactNode, useCallback, useEffect, useMemo } from 'react'
|
||||
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
Separator,
|
||||
Skeleton,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from 'ui'
|
||||
import { Button, cn, Separator, Skeleton, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
import { Kbd } from './primitives/Kbd'
|
||||
import { useDataTable } from './providers/DataTableProvider'
|
||||
|
||||
@@ -79,44 +70,41 @@ export function DataTableSheetDetails({
|
||||
{isLoading && !selectedRowKey ? <Skeleton className="h-7 w-36" /> : title}
|
||||
</h5>
|
||||
<div className="flex h-7 items-center gap-1">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="text"
|
||||
disabled={!prevId}
|
||||
onClick={onPrev}
|
||||
className="px-1"
|
||||
icon={<ChevronUp />}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Navigate <Kbd>↑</Kbd>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="text"
|
||||
disabled={!nextId}
|
||||
onClick={onNext}
|
||||
className="px-1"
|
||||
icon={<ChevronDown />}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Navigate <Kbd>↓</Kbd>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="text"
|
||||
disabled={!prevId}
|
||||
onClick={onPrev}
|
||||
className="px-1"
|
||||
icon={<ChevronUp />}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Navigate <Kbd>↑</Kbd>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="text"
|
||||
disabled={!nextId}
|
||||
onClick={onNext}
|
||||
className="px-1"
|
||||
icon={<ChevronDown />}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
Navigate <Kbd>↓</Kbd>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Separator orientation="vertical" className="mx-1" />
|
||||
|
||||
<Button
|
||||
|
||||
@@ -2,7 +2,7 @@ import { PanelLeftClose, PanelLeftOpen } from 'lucide-react'
|
||||
import { ReactNode, useMemo } from 'react'
|
||||
|
||||
import { useHotKey } from 'hooks/ui/useHotKey'
|
||||
import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from 'ui'
|
||||
import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
import { formatCompactNumber } from './DataTable.utils'
|
||||
import { DataTableFilterControlsDrawer } from './DataTableFilters/DataTableFilterControlsDrawer'
|
||||
import { DataTableResetButton } from './DataTableResetButton'
|
||||
@@ -33,30 +33,28 @@ export function DataTableToolbar({ renderActions }: DataTableToolbarProps) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="outline"
|
||||
icon={open ? <PanelLeftClose /> : <PanelLeftOpen />}
|
||||
onClick={() => setOpen((prev) => !prev)}
|
||||
className="hidden sm:flex"
|
||||
>
|
||||
<span className="hidden md:block">{open ? 'Hide' : 'Show'} Controls</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>
|
||||
Toggle controls with{' '}
|
||||
<Kbd className="ml-1 text-muted-foreground group-hover:text-accent-foreground">
|
||||
<span className="mr-1">⌘</span>
|
||||
<span>B</span>
|
||||
</Kbd>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="outline"
|
||||
icon={open ? <PanelLeftClose /> : <PanelLeftOpen />}
|
||||
onClick={() => setOpen((prev) => !prev)}
|
||||
className="hidden sm:flex"
|
||||
>
|
||||
<span className="hidden md:block">{open ? 'Hide' : 'Show'} Controls</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
<p>
|
||||
Toggle controls with{' '}
|
||||
<Kbd className="ml-1 text-muted-foreground group-hover:text-accent-foreground">
|
||||
<span className="mr-1">⌘</span>
|
||||
<span>B</span>
|
||||
</Kbd>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="block sm:hidden">
|
||||
<DataTableFilterControlsDrawer />
|
||||
</div>
|
||||
|
||||
@@ -13,4 +13,6 @@ export const replicationKeys = {
|
||||
['projects', projectRef, 'pipelines', pipelineId] as const,
|
||||
pipelinesStatus: (projectRef: string | undefined, pipelineId: number | undefined) =>
|
||||
['projects', projectRef, 'pipelines', pipelineId, 'status'] as const,
|
||||
pipelinesReplicationStatus: (projectRef: string | undefined, pipelineId: number | undefined) =>
|
||||
['projects', projectRef, 'pipelines', pipelineId, 'replication-status'] as const,
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { UseQueryOptions, useQuery } from '@tanstack/react-query'
|
||||
|
||||
import { get, handleError } from 'data/fetchers'
|
||||
import { ResponseError } from 'types'
|
||||
import { replicationKeys } from './keys'
|
||||
|
||||
type ReplicationPipelineReplicationStatusParams = { projectRef?: string; pipelineId?: number }
|
||||
|
||||
async function fetchReplicationPipelineReplicationStatus(
|
||||
{ projectRef, pipelineId }: ReplicationPipelineReplicationStatusParams,
|
||||
signal?: AbortSignal
|
||||
) {
|
||||
if (!projectRef) throw new Error('projectRef is required')
|
||||
if (!pipelineId) throw new Error('pipelineId is required')
|
||||
|
||||
const { data, error } = await get(
|
||||
'/platform/replication/{ref}/pipelines/{pipeline_id}/replication-status',
|
||||
{
|
||||
params: { path: { ref: projectRef, pipeline_id: pipelineId } },
|
||||
signal,
|
||||
}
|
||||
)
|
||||
if (error) {
|
||||
handleError(error)
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export type ReplicationPipelineReplicationStatusData = Awaited<
|
||||
ReturnType<typeof fetchReplicationPipelineReplicationStatus>
|
||||
>
|
||||
|
||||
export const useReplicationPipelineReplicationStatusQuery = <
|
||||
TData = ReplicationPipelineReplicationStatusData,
|
||||
>(
|
||||
{ projectRef, pipelineId }: ReplicationPipelineReplicationStatusParams,
|
||||
{
|
||||
enabled = true,
|
||||
...options
|
||||
}: UseQueryOptions<ReplicationPipelineReplicationStatusData, ResponseError, TData> = {}
|
||||
) =>
|
||||
useQuery<ReplicationPipelineReplicationStatusData, ResponseError, TData>(
|
||||
replicationKeys.pipelinesReplicationStatus(projectRef, pipelineId),
|
||||
({ signal }) => fetchReplicationPipelineReplicationStatus({ projectRef, pipelineId }, signal),
|
||||
{
|
||||
enabled: enabled && typeof projectRef !== 'undefined' && typeof pipelineId !== 'undefined',
|
||||
...options,
|
||||
}
|
||||
)
|
||||
@@ -1,49 +0,0 @@
|
||||
import ReplicationComingSoon from 'components/interfaces/Database/Replication/ComingSoon'
|
||||
import Destinations from 'components/interfaces/Database/Replication/Destinations'
|
||||
import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import {
|
||||
ScaffoldContainer,
|
||||
ScaffoldDescription,
|
||||
ScaffoldHeader,
|
||||
ScaffoldTitle,
|
||||
} from 'components/layouts/Scaffold'
|
||||
import { useFlag } from 'hooks/ui/useFlag'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
|
||||
const DatabaseReplicationPage: NextPageWithLayout = () => {
|
||||
const enablePgReplicate = useFlag('enablePgReplicate')
|
||||
|
||||
return (
|
||||
<>
|
||||
{enablePgReplicate ? (
|
||||
<>
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldHeader>
|
||||
<ScaffoldTitle>Replication</ScaffoldTitle>
|
||||
<Destinations />
|
||||
</ScaffoldHeader>
|
||||
</ScaffoldContainer>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldHeader>
|
||||
<ScaffoldTitle>Replication</ScaffoldTitle>
|
||||
<ScaffoldDescription>Send data to other destinations</ScaffoldDescription>
|
||||
</ScaffoldHeader>
|
||||
</ScaffoldContainer>
|
||||
<ReplicationComingSoon />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
DatabaseReplicationPage.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<DatabaseLayout title="Database Replication">{page}</DatabaseLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
export default DatabaseReplicationPage
|
||||
@@ -0,0 +1,76 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import { ReplicationComingSoon } from 'components/interfaces/Database/Replication/ComingSoon'
|
||||
import { Destinations } from 'components/interfaces/Database/Replication/Destinations'
|
||||
import { ReplicationPipelineStatus } from 'components/interfaces/Database/Replication/ReplicationPipelineStatus'
|
||||
import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
|
||||
import { FormHeader } from 'components/ui/Forms/FormHeader'
|
||||
import { useFlag } from 'hooks/ui/useFlag'
|
||||
import { PipelineRequestStatusProvider } from 'state/replication-pipeline-request-status'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
|
||||
const DatabaseReplicationPage: NextPageWithLayout = () => {
|
||||
const enablePgReplicate = useFlag('enablePgReplicate')
|
||||
const [selectedPipelineId, setSelectedPipelineId] = useState<number>()
|
||||
const [selectedDestinationName, setSelectedDestinationName] = useState<string>()
|
||||
|
||||
// [Joshen] Ideally selecting a pipeline should be a route on its own with pipelineId as the param
|
||||
// e.g /project/ref/database/replication/[pipelineId]
|
||||
// Can destinationName be derived from pipeline ID or something?
|
||||
|
||||
const handleSelectPipeline = (pipelineId: number, destinationName: string) => {
|
||||
setSelectedPipelineId(pipelineId)
|
||||
setSelectedDestinationName(destinationName)
|
||||
}
|
||||
|
||||
const handleSelectBack = () => {
|
||||
setSelectedPipelineId(undefined)
|
||||
setSelectedDestinationName(undefined)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{enablePgReplicate ? (
|
||||
<PipelineRequestStatusProvider>
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldSection>
|
||||
<div className="col-span-12">
|
||||
<FormHeader title="Replication" />
|
||||
{selectedPipelineId === undefined ? (
|
||||
<Destinations onSelectPipeline={handleSelectPipeline} />
|
||||
) : (
|
||||
<ReplicationPipelineStatus
|
||||
pipelineId={selectedPipelineId}
|
||||
destinationName={selectedDestinationName}
|
||||
onSelectBack={handleSelectBack}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ScaffoldSection>
|
||||
</ScaffoldContainer>
|
||||
</PipelineRequestStatusProvider>
|
||||
) : (
|
||||
<>
|
||||
<ScaffoldContainer>
|
||||
<ScaffoldSection>
|
||||
<div className="col-span-12">
|
||||
<FormHeader title="Replication" description="Send data to other destinations" />
|
||||
</div>
|
||||
</ScaffoldSection>
|
||||
</ScaffoldContainer>
|
||||
<ReplicationComingSoon />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
DatabaseReplicationPage.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<DatabaseLayout title="Database Replication">{page}</DatabaseLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
export default DatabaseReplicationPage
|
||||
@@ -0,0 +1,76 @@
|
||||
import { createContext, useContext, useState, ReactNode, useCallback } from 'react'
|
||||
|
||||
export enum PipelineStatusRequestStatus {
|
||||
None = 'None',
|
||||
EnableRequested = 'EnableRequested',
|
||||
DisableRequested = 'DisableRequested',
|
||||
}
|
||||
|
||||
interface PipelineRequestStatusContextType {
|
||||
requestStatus: Record<number, PipelineStatusRequestStatus>
|
||||
setRequestStatus: (pipelineId: number, status: PipelineStatusRequestStatus) => void
|
||||
getRequestStatus: (pipelineId: number) => PipelineStatusRequestStatus
|
||||
updatePipelineStatus: (pipelineId: number, backendStatus: string | undefined) => void
|
||||
}
|
||||
|
||||
interface PipelineRequestStatusProviderProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const PipelineRequestStatusContext = createContext<PipelineRequestStatusContextType | undefined>(
|
||||
undefined
|
||||
)
|
||||
|
||||
export const PipelineRequestStatusProvider = ({ children }: PipelineRequestStatusProviderProps) => {
|
||||
const [requestStatus, setRequestStatusState] = useState<
|
||||
Record<number, PipelineStatusRequestStatus>
|
||||
>({})
|
||||
|
||||
const setRequestStatus = (pipelineId: number, status: PipelineStatusRequestStatus) => {
|
||||
setRequestStatusState((prev) => ({
|
||||
...prev,
|
||||
[pipelineId]: status,
|
||||
}))
|
||||
}
|
||||
|
||||
const getRequestStatus = (pipelineId: number): PipelineStatusRequestStatus => {
|
||||
return requestStatus[pipelineId] || PipelineStatusRequestStatus.None
|
||||
}
|
||||
|
||||
const updatePipelineStatus = useCallback(
|
||||
(pipelineId: number, backendStatus: string | undefined) => {
|
||||
const currentRequestStatus = requestStatus[pipelineId] || PipelineStatusRequestStatus.None
|
||||
|
||||
if (
|
||||
(currentRequestStatus === PipelineStatusRequestStatus.EnableRequested &&
|
||||
(backendStatus === 'started' || backendStatus === 'failed')) ||
|
||||
(currentRequestStatus === PipelineStatusRequestStatus.DisableRequested &&
|
||||
(backendStatus === 'stopped' || backendStatus === 'failed'))
|
||||
) {
|
||||
setRequestStatus(pipelineId, PipelineStatusRequestStatus.None)
|
||||
}
|
||||
},
|
||||
[requestStatus, setRequestStatus]
|
||||
)
|
||||
|
||||
return (
|
||||
<PipelineRequestStatusContext.Provider
|
||||
value={{
|
||||
requestStatus,
|
||||
setRequestStatus,
|
||||
getRequestStatus,
|
||||
updatePipelineStatus,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PipelineRequestStatusContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const usePipelineRequestStatus = () => {
|
||||
const context = useContext(PipelineRequestStatusContext)
|
||||
if (context === undefined) {
|
||||
throw new Error('usePipelineRequestStatus must be used within a PipelineRequestStatusProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
@@ -119,7 +119,7 @@
|
||||
}
|
||||
|
||||
.table-container thead th:first-child {
|
||||
@apply pl-6 rounded rounded-r-none rounded-b-none;
|
||||
@apply rounded rounded-r-none rounded-b-none;
|
||||
@apply border-l;
|
||||
}
|
||||
|
||||
|
||||
+156
@@ -3391,6 +3391,23 @@ export interface paths {
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/platform/replication/{ref}/pipelines/{pipeline_id}/replication-status': {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path?: never
|
||||
cookie?: never
|
||||
}
|
||||
/** Get the status of a replication pipeline. */
|
||||
get: operations['ReplicationPipelinesController_getPipelineReplicationStatus']
|
||||
put?: never
|
||||
post?: never
|
||||
delete?: never
|
||||
options?: never
|
||||
head?: never
|
||||
patch?: never
|
||||
trace?: never
|
||||
}
|
||||
'/platform/replication/{ref}/pipelines/{pipeline_id}/start': {
|
||||
parameters: {
|
||||
query?: never
|
||||
@@ -7353,6 +7370,41 @@ export interface components {
|
||||
tenant_id: string
|
||||
}[]
|
||||
}
|
||||
ReplicationPipelineReplicationStatusResponse: {
|
||||
/** @description Pipeline id */
|
||||
pipeline_id: number
|
||||
/** @description Table statuses */
|
||||
table_statuses: {
|
||||
/** @description Table replication state */
|
||||
state:
|
||||
| {
|
||||
/** @enum {string} */
|
||||
name: 'queued'
|
||||
}
|
||||
| {
|
||||
/** @enum {string} */
|
||||
name: 'copying_table'
|
||||
}
|
||||
| {
|
||||
/** @enum {string} */
|
||||
name: 'copied_table'
|
||||
}
|
||||
| {
|
||||
lag: number
|
||||
/** @enum {string} */
|
||||
name: 'following_wal'
|
||||
}
|
||||
| {
|
||||
message: string
|
||||
/** @enum {string} */
|
||||
name: 'error'
|
||||
}
|
||||
/** @description Table id (internal Postgres OID) */
|
||||
table_id: number
|
||||
/** @description Table name */
|
||||
table_name: string
|
||||
}[]
|
||||
}
|
||||
ReplicationPipelineResponse: {
|
||||
/** @description Pipeline config */
|
||||
config: {
|
||||
@@ -16607,6 +16659,12 @@ export interface operations {
|
||||
'application/json': components['schemas']['GetUserContentResponse']
|
||||
}
|
||||
}
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Failed to retrieve project's content */
|
||||
500: {
|
||||
headers: {
|
||||
@@ -16638,6 +16696,12 @@ export interface operations {
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Failed to update project's content */
|
||||
500: {
|
||||
headers: {
|
||||
@@ -16671,6 +16735,12 @@ export interface operations {
|
||||
'application/json': components['schemas']['UserContentObject']
|
||||
}
|
||||
}
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Failed to create project's content */
|
||||
500: {
|
||||
headers: {
|
||||
@@ -16702,6 +16772,12 @@ export interface operations {
|
||||
'application/json': components['schemas']['BulkDeleteUserContentResponse'][]
|
||||
}
|
||||
}
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Failed to delete project's contents */
|
||||
500: {
|
||||
headers: {
|
||||
@@ -16734,6 +16810,12 @@ export interface operations {
|
||||
'application/json': components['schemas']['GetContentCountV2Response']
|
||||
}
|
||||
}
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Failed to retrieve user's content counts */
|
||||
500: {
|
||||
headers: {
|
||||
@@ -16771,6 +16853,12 @@ export interface operations {
|
||||
'application/json': components['schemas']['GetUserContentFolderResponse']
|
||||
}
|
||||
}
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Failed to retrieve project's content root folder */
|
||||
500: {
|
||||
headers: {
|
||||
@@ -16804,6 +16892,12 @@ export interface operations {
|
||||
'application/json': components['schemas']['CreateUserContentFolderResponse']
|
||||
}
|
||||
}
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Failed to create project's content folder */
|
||||
500: {
|
||||
headers: {
|
||||
@@ -16830,6 +16924,12 @@ export interface operations {
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Failed to delete project's content folders */
|
||||
500: {
|
||||
headers: {
|
||||
@@ -16867,6 +16967,12 @@ export interface operations {
|
||||
'application/json': components['schemas']['GetUserContentFolderResponse']
|
||||
}
|
||||
}
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Failed to retrieve project's content folder */
|
||||
500: {
|
||||
headers: {
|
||||
@@ -16900,6 +17006,12 @@ export interface operations {
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Failed to update project's content folder */
|
||||
500: {
|
||||
headers: {
|
||||
@@ -16930,6 +17042,12 @@ export interface operations {
|
||||
'application/json': components['schemas']['GetUserContentByIdResponse']
|
||||
}
|
||||
}
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Failed to retrieve project's content by the given id */
|
||||
500: {
|
||||
headers: {
|
||||
@@ -18639,6 +18757,44 @@ export interface operations {
|
||||
}
|
||||
}
|
||||
}
|
||||
ReplicationPipelinesController_getPipelineReplicationStatus: {
|
||||
parameters: {
|
||||
query?: never
|
||||
header?: never
|
||||
path: {
|
||||
/** @description Pipeline id */
|
||||
pipeline_id: number
|
||||
/** @description Project ref */
|
||||
ref: string
|
||||
}
|
||||
cookie?: never
|
||||
}
|
||||
requestBody?: never
|
||||
responses: {
|
||||
/** @description Returns the pipeline replication status. */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content: {
|
||||
'application/json': components['schemas']['ReplicationPipelineReplicationStatusResponse']
|
||||
}
|
||||
}
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
/** @description Failed to get pipeline replication status. */
|
||||
500: {
|
||||
headers: {
|
||||
[name: string]: unknown
|
||||
}
|
||||
content?: never
|
||||
}
|
||||
}
|
||||
}
|
||||
ReplicationPipelinesController_startPipeline: {
|
||||
parameters: {
|
||||
query?: never
|
||||
|
||||
Reference in new issue
Block a user