Files
supabase/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationPanel.tsx
T
Joshen Lim 09a1f431bc Mass update for all lucide icon imports to use the component with Icon suffix (#51659)
## Context

This is largely just a mechanical change here:
Opting to use lucide icons with the `Icon` suffix across all icon
imports within `studio`
- so that it’s clearer which components are icons + reduces the
possibility of name collision with our own components
- e.g <User /> vs <UserIcon />

Also added a eslint rule for this:
<img width="1069" height="167" alt="image"
src="https://github.com/user-attachments/assets/7f36f443-5ca2-4bf6-8d22-86e774513bfc"
/>

And updated `AGENTS.md` as such:
```
Icons come from `lucide-react`, always imported with the `Icon` suffix (`import { CheckIcon, Trash2Icon } from 'lucide-react'`) — never the bare names (`Check`) or `Check as CheckIcon` aliases (lint-enforced).
```
2026-10-14 12:56:55 +02:00

228 lines
8.0 KiB
TypeScript

import { useFeatureFlags, useParams } from 'common'
import { ArrowUpRightIcon } from 'lucide-react'
import Link from 'next/link'
import { parseAsInteger, parseAsStringEnum, useQueryState } from 'nuqs'
import { useEffect, useRef } from 'react'
import { toast } from 'sonner'
import {
Button,
DialogSectionSeparator,
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetSection,
SheetTitle,
} from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import { EnablePipelinesCallout } from '../EnablePipelinesCallout'
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 { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialogs/DiscardChangesConfirmationDialog'
import { DocsButton } from '@/components/ui/DocsButton'
import { useReplicationDestinationsQuery } from '@/data/replication/destinations-query'
import { checkLocalETLNotSetUp } from '@/data/replication/utils'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useConfirmOnClose } from '@/hooks/ui/useConfirmOnClose'
import { DOCS_URL, IS_PLATFORM } from '@/lib/constants'
const DESTINATION_DOCS_PATHS: Partial<Record<DestinationType, string>> = {
BigQuery: '/guides/database/replication/pipelines/bigquery#configure-bigquery-as-a-destination',
ClickHouse:
'/guides/database/replication/pipelines/clickhouse#configure-clickhouse-as-a-destination',
DuckLake: '/guides/database/replication/pipelines/ducklake#choose-a-configuration-mode',
Snowflake: '/guides/database/replication/pipelines/snowflake#prepare-snowflake-resources',
}
export const DestinationPanel = () => {
const { ref: projectRef } = useParams()
const { hasLoaded: flagsLoaded } = useFeatureFlags()
const { isPending: isOrganizationPending } = useSelectedOrganizationQuery()
const enablePgReplicate = useIsETLPrivateAlpha()
const isAccessLoading = IS_PLATFORM && (!flagsLoaded || isOrganizationPending)
const { error: destinationsError } = useReplicationDestinationsQuery({ projectRef })
const isLocalETLNotSetUp = checkLocalETLNotSetUp(destinationsError)
const [urlDestinationType, setDestinationType] = useQueryState(
'destinationType',
parseAsStringEnum<DestinationType>([
'BigQuery',
'Analytics Bucket',
'DuckLake',
'Snowflake',
'ClickHouse',
]).withOptions({
history: 'push',
clearOnDefault: true,
})
)
const [edit, setEdit] = useQueryState(
'edit',
parseAsInteger.withOptions({
history: 'push',
clearOnDefault: true,
})
)
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 showAccessRequest = !isAccessLoading && !enablePgReplicate
const showEnablement = !isAccessLoading && enablePgReplicate && replicationNotEnabled
const showDestinationForm = !isAccessLoading && enablePgReplicate && !replicationNotEnabled
const existingDestination = editMode
? {
sourceId,
destinationId: edit,
pipelineId: pipeline?.id,
statusName,
enabled:
statusName === PipelineStatusName.STARTED || statusName === PipelineStatusName.FAILED,
}
: undefined
const checkIsDirtyRef = useRef<() => boolean>(() => false)
const onClose = () => {
checkIsDirtyRef.current = () => false
setDestinationType(null)
setEdit(null)
}
const { confirmOnClose, handleOpenChange, modalProps } = useConfirmOnClose({
checkIsDirty: () => checkIsDirtyRef.current(),
onClose,
})
const docsUrl = `${DOCS_URL}${
(destinationType && DESTINATION_DOCS_PATHS[destinationType]) ??
'/guides/database/replication/pipelines#step-3-configure-a-destination'
}`
useEffect(() => {
if (edit !== null && invalidExistingDestination) {
toast(`Unable to find destination ID ${edit}`)
setEdit(null)
}
}, [edit, invalidExistingDestination, setEdit])
const typeSelection = (
<>
<DestinationTypeSelection />
<DialogSectionSeparator />
</>
)
const pipelinesTypeSelection = (
<>
{typeSelection}
{destinationType != null && isLocalETLNotSetUp && (
<SheetSection className="pb-0!">
<Admonition
type="warning"
title="Replication unavailable locally"
description="Configure the replication API to manage pipelines in local development."
/>
</SheetSection>
)}
</>
)
return (
<>
<Sheet open={visible} onOpenChange={handleOpenChange}>
<SheetContent size="lg" showClose={false} className="max-w-3xl">
<div className="flex flex-col h-full min-h-0" tabIndex={-1}>
<SheetHeader className="flex items-center justify-between">
<div>
<SheetTitle>{editMode ? 'Edit pipeline' : 'Add pipeline'}</SheetTitle>
<SheetDescription>
{editMode
? 'Update how this pipeline sends data to its destination.'
: 'Send tables to an external destination for analytics workloads.'}
</SheetDescription>
</div>
<DocsButton
href={docsUrl}
topic={`${destinationType ?? 'destination'} pipeline settings`}
/>
</SheetHeader>
{isAccessLoading && (
<SheetSection>
<GenericSkeletonLoader />
</SheetSection>
)}
{showAccessRequest && (
<div className="grow overflow-auto min-h-0">
{pipelinesTypeSelection}
<SheetSection>
<Admonition
type="note"
layout="responsive"
title="Request Pipelines access"
description="Pipelines is in public alpha and available to approved organizations."
actions={
<Button
asChild
variant="primary"
iconRight={<ArrowUpRightIcon size={16} strokeWidth={1.5} />}
>
<Link
target="_blank"
rel="noreferrer"
href="https://forms.supabase.com/pg_replicate"
>
Request access
</Link>
</Button>
}
/>
</SheetSection>
</div>
)}
{showEnablement && (
<div className="grow overflow-auto min-h-0">
{pipelinesTypeSelection}
<SheetSection>
<EnablePipelinesCallout type={destinationType} />
</SheetSection>
</div>
)}
{showDestinationForm && (
<DestinationForm
visible={visible}
selectedType={destinationType ?? 'BigQuery'}
existingDestination={existingDestination}
typeSelection={pipelinesTypeSelection}
checkIsDirtyRef={checkIsDirtyRef}
onClose={onClose}
onCancel={confirmOnClose}
/>
)}
</div>
</SheetContent>
</Sheet>
<DiscardChangesConfirmationDialog {...modalProps} />
</>
)
}