| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661 |
- // @ts-nocheck
- import { zodResolver } from '@hookform/resolvers/zod'
- import { PermissionAction } from '@supabase/shared-types/out/constants'
- import { useParams } from 'common'
- import { AnimatePresence, motion } from 'framer-motion'
- import { Loader2 } from 'lucide-react'
- import { useEffect, useMemo, useRef, useState } from 'react'
- import { useForm } from 'react-hook-form'
- import { toast } from 'sonner'
- import { Button, DialogSectionSeparator, Form, SheetFooter, SheetSection } from 'ui'
- import * as z from 'zod'
- import {
- useIsETLBigQueryPrivateAlpha,
- useIsETLDucklakePrivateAlpha,
- useIsETLIcebergPrivateAlpha,
- } from '../../useIsETLPrivateAlpha'
- import { DestinationType } from '../DestinationPanel.types'
- import { AdvancedSettings } from './AdvancedSettings'
- import { CREATE_NEW_NAMESPACE } from './DestinationForm.constants'
- import { DestinationPanelFormSchema as FormSchema } from './DestinationForm.schema'
- import {
- buildDestinationConfig,
- buildDestinationConfigForValidation,
- getDucklakeValidationIssues,
- } from './DestinationForm.utils'
- import { DestinationNameInput } from './DestinationNameInput'
- import { AnalyticsBucketFields, BigQueryFields, DuckLakeFields } from './DestinationPanelFields'
- import { NewPublicationPanel } from './NewPublicationPanel'
- import { NoDestinationsAvailable } from './NoDestinationsAvailable'
- import { PublicationSelection } from './PublicationSelection'
- import { ReplicationDisclaimerDialog } from './ReplicationDisclaimerDialog'
- import { ValidationFailuresSection } from './ValidationFailuresSection'
- import { CreateAnalyticsBucketSheet } from '@/components/interfaces/Storage/AnalyticsBuckets/CreateAnalyticsBucketSheet'
- import { getKeys, useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
- import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
- import {
- BatchConfig,
- useCreateDestinationPipelineMutation,
- } from '@/data/replication/create-destination-pipeline-mutation'
- import { useReplicationDestinationByIdQuery } from '@/data/replication/destination-by-id-query'
- import { useReplicationPipelineByIdQuery } from '@/data/replication/pipeline-by-id-query'
- import { useReplicationPublicationsQuery } from '@/data/replication/publications-query'
- import { useRestartPipelineHelper } from '@/data/replication/restart-pipeline-helper'
- import { useReplicationSourcesQuery } from '@/data/replication/sources-query'
- import { useStartPipelineMutation } from '@/data/replication/start-pipeline-mutation'
- import { useUpdateDestinationPipelineMutation } from '@/data/replication/update-destination-pipeline-mutation'
- import {
- useValidateDestinationMutation,
- type ValidationFailure,
- } from '@/data/replication/validate-destination-mutation'
- import { useValidatePipelineMutation } from '@/data/replication/validate-pipeline-mutation'
- import { useIcebergNamespaceCreateMutation } from '@/data/storage/iceberg-namespace-create-mutation'
- import { useS3AccessKeyCreateMutation } from '@/data/storage/s3-access-key-create-mutation'
- import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
- import {
- PipelineStatusRequestStatus,
- usePipelineRequestStatus,
- } from '@/state/replication-pipeline-request-status'
- import { type ResponseError } from '@/types'
- const formId = 'destination-editor'
- interface DestinationFormProps {
- selectedType: DestinationType
- visible: boolean
- existingDestination?: {
- sourceId?: number
- destinationId: number
- pipelineId?: number
- enabled: boolean
- statusName?: string
- }
- onClose: () => void
- }
- type DucklakeApiConfig = {
- catalog_url: string
- data_path: string
- pool_size?: number
- s3_access_key_id?: string
- s3_secret_access_key?: string
- s3_region?: string
- s3_endpoint?: string
- s3_url_style?: 'path' | 'vhost'
- s3_use_ssl?: boolean
- metadata_schema?: string
- expire_snapshots_older_than?: string
- }
- export const DestinationForm = ({
- selectedType,
- visible,
- existingDestination,
- onClose,
- }: DestinationFormProps) => {
- const { ref: projectRef } = useParams()
- const { setRequestStatus } = usePipelineRequestStatus()
- const etlEnableBigQuery = useIsETLBigQueryPrivateAlpha()
- const etlEnableIceberg = useIsETLIcebergPrivateAlpha()
- const etlEnableDucklake = useIsETLDucklakePrivateAlpha()
- const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*')
- const [isFormInteracting, setIsFormInteracting] = useState(false)
- const [showDisclaimerDialog, setShowDisclaimerDialog] = useState(false)
- const [publicationPanelVisible, setPublicationPanelVisible] = useState(false)
- const [newBucketSheetVisible, setNewBucketSheetVisible] = useState(false)
- const [pendingFormValues, setPendingFormValues] = useState<z.infer<typeof FormSchema> | null>(
- null
- )
- const [hasRunValidation, setHasRunValidation] = useState(false)
- const [destinationValidationFailures, setDestinationValidationFailures] = useState<
- ValidationFailure[]
- >([])
- const [pipelineValidationFailures, setPipelineValidationFailures] = useState<ValidationFailure[]>(
- []
- )
- const validationSectionRef = useRef<HTMLDivElement>(null)
- const editMode = !!existingDestination
- // Compute available destinations based on feature flags
- const availableDestinations = useMemo(() => {
- const destinations = []
- if (etlEnableBigQuery) destinations.push({ value: 'BigQuery', label: 'BigQuery' })
- if (etlEnableIceberg)
- destinations.push({ value: 'Analytics Bucket', label: 'Analytics Bucket' })
- if (etlEnableDucklake) destinations.push({ value: 'DuckLake', label: 'DuckLake' })
- return destinations
- }, [etlEnableBigQuery, etlEnableDucklake, etlEnableIceberg])
- const hasNoAvailableDestinations = availableDestinations.length === 0
- const { data: sourcesData } = useReplicationSourcesQuery({ projectRef })
- const sourceId = sourcesData?.sources.find((s) => s.name === projectRef)?.id
- const {
- data: publications = [],
- isSuccess: isSuccessPublications,
- refetch: refetchPublications,
- } = useReplicationPublicationsQuery({ projectRef, sourceId })
- const { data: destinationData } = useReplicationDestinationByIdQuery({
- projectRef,
- destinationId: existingDestination?.destinationId,
- })
- const { data: pipelineData } = useReplicationPipelineByIdQuery({
- projectRef,
- pipelineId: existingDestination?.pipelineId,
- })
- const { data: apiKeys } = useAPIKeysQuery(
- { projectRef, reveal: true },
- { enabled: canReadAPIKeys }
- )
- const { serviceKey } = getKeys(apiKeys)
- const catalogToken = serviceKey?.api_key ?? ''
- const { data: projectSettings } = useProjectSettingsV2Query({ projectRef })
- const { mutateAsync: createDestinationPipeline, isPending: creatingDestinationPipeline } =
- useCreateDestinationPipelineMutation({
- onSuccess: () => form.reset(defaultValues),
- })
- const { mutateAsync: updateDestinationPipeline, isPending: updatingDestinationPipeline } =
- useUpdateDestinationPipelineMutation({
- onSuccess: () => form.reset(defaultValues),
- })
- const { mutateAsync: startPipeline, isPending: startingPipeline } = useStartPipelineMutation()
- const { restartPipeline } = useRestartPipelineHelper()
- const { mutateAsync: createS3AccessKey, isPending: isCreatingS3AccessKey } =
- useS3AccessKeyCreateMutation()
- const { mutateAsync: createNamespace, isPending: isCreatingNamespace } =
- useIcebergNamespaceCreateMutation()
- const { mutateAsync: validateDestination, isPending: isValidatingDestination } =
- useValidateDestinationMutation()
- const { mutateAsync: validatePipeline, isPending: isValidatingPipeline } =
- useValidatePipelineMutation()
- const isValidating = isValidatingDestination || isValidatingPipeline
- const defaultValues = useMemo(() => {
- const config = destinationData?.config
- const isBigQueryConfig = config && 'big_query' in config
- const isIcebergConfig = config && 'iceberg' in config
- const ducklakeConfigValue =
- config && 'ducklake' in (config as Record<string, unknown>)
- ? (config as Record<string, unknown>).ducklake
- : undefined
- const ducklakeConfig =
- ducklakeConfigValue && typeof ducklakeConfigValue === 'object'
- ? (ducklakeConfigValue as DucklakeApiConfig)
- : undefined
- return {
- // Common fields
- name: destinationData?.name ?? '',
- publicationName: pipelineData?.config.publication_name ?? '',
- maxFillMs: pipelineData?.config?.batch?.max_fill_ms ?? undefined,
- maxTableSyncWorkers: pipelineData?.config?.max_table_sync_workers ?? undefined,
- maxCopyConnectionsPerTable: pipelineData?.config?.max_copy_connections_per_table ?? undefined,
- invalidatedSlotBehavior:
- (pipelineData?.config as { invalidated_slot_behavior?: 'error' | 'recreate' } | undefined)
- ?.invalidated_slot_behavior ?? undefined,
- // BigQuery fields
- projectId: isBigQueryConfig ? config.big_query.project_id : '',
- datasetId: isBigQueryConfig ? config.big_query.dataset_id : '',
- serviceAccountKey: isBigQueryConfig ? config.big_query.service_account_key : '',
- connectionPoolSize:
- (config as { big_query?: { connection_pool_size?: number } } | undefined)?.big_query
- ?.connection_pool_size ?? undefined,
- maxStalenessMins: isBigQueryConfig ? config.big_query.max_staleness_mins : undefined, // Default: null
- // Analytics Bucket fields
- warehouseName: isIcebergConfig ? config.iceberg.briven.warehouse_name : '',
- namespace: isIcebergConfig ? config.iceberg.briven.namespace : '',
- newNamespaceName: '',
- catalogToken: isIcebergConfig ? config.iceberg.briven.catalog_token : catalogToken,
- s3AccessKeyId: isIcebergConfig ? config.iceberg.briven.s3_access_key_id : '',
- s3SecretAccessKey: isIcebergConfig ? config.iceberg.briven.s3_secret_access_key : '',
- s3Region:
- projectSettings?.region ?? (isIcebergConfig ? config.iceberg.briven.s3_region : ''),
- // DuckLake fields
- ducklakeCatalogUrl: ducklakeConfig?.catalog_url ?? '',
- ducklakeDataPath: ducklakeConfig?.data_path ?? '',
- ducklakePoolSize: ducklakeConfig?.pool_size,
- ducklakeS3AccessKeyId: ducklakeConfig?.s3_access_key_id ?? '',
- ducklakeS3SecretAccessKey: ducklakeConfig?.s3_secret_access_key ?? '',
- ducklakeS3Region: ducklakeConfig?.s3_region ?? '',
- ducklakeS3Endpoint: ducklakeConfig?.s3_endpoint ?? '',
- ducklakeS3UrlStyle: ducklakeConfig?.s3_url_style ?? 'path',
- ducklakeS3UseSsl: ducklakeConfig?.s3_use_ssl ?? true,
- ducklakeMetadataSchema: ducklakeConfig?.metadata_schema ?? 'ducklake',
- ducklakeExpireSnapshotsOlderThan: ducklakeConfig?.expire_snapshots_older_than ?? '',
- }
- }, [destinationData, pipelineData, catalogToken, projectSettings])
- const form = useForm<z.infer<typeof FormSchema>>({
- mode: 'onChange',
- reValidateMode: 'onChange',
- resolver: zodResolver(
- FormSchema.superRefine((data, ctx: any) => {
- const addRequiredFieldError = (path: string, message: string) => {
- ctx.addIssue({
- code: z.ZodIssueCode.custom,
- message,
- path: [path],
- })
- }
- if (selectedType === 'BigQuery') {
- if (!data.projectId?.length) addRequiredFieldError('projectId', 'Project ID is required')
- if (!data.datasetId?.length) addRequiredFieldError('datasetId', 'Dataset ID is required')
- if (!data.serviceAccountKey?.length)
- addRequiredFieldError('serviceAccountKey', 'Service Account Key is required')
- } else if (selectedType === 'Analytics Bucket') {
- if (!data.warehouseName?.length)
- addRequiredFieldError('warehouseName', 'Bucket is required')
- const hasValidNamespace =
- (data.namespace?.length && data.namespace !== 'create-new-namespace') ||
- (data.namespace === 'create-new-namespace' && data.newNamespaceName?.length)
- if (!hasValidNamespace) {
- const isCreatingNew = data.namespace === 'create-new-namespace'
- addRequiredFieldError(
- isCreatingNew ? 'newNamespaceName' : 'namespace',
- isCreatingNew ? 'Namespace name is required' : 'Namespace is required'
- )
- }
- if (!data.s3Region?.length) addRequiredFieldError('s3Region', 'S3 Region is required')
- if (!data.s3AccessKeyId?.length)
- addRequiredFieldError('s3AccessKeyId', 'S3 Access Key ID is required')
- if (data.s3AccessKeyId !== 'create-new' && !data.s3SecretAccessKey?.length) {
- addRequiredFieldError('s3SecretAccessKey', 'S3 Secret Access Key is required')
- }
- } else if (selectedType === 'DuckLake') {
- getDucklakeValidationIssues(data).forEach(({ path, message }) => {
- addRequiredFieldError(path, message)
- })
- }
- })
- ),
- defaultValues,
- })
- const { publicationName, warehouseName } = form.watch()
- const publicationNames = useMemo(() => publications?.map((pub) => pub.name) ?? [], [publications])
- const isSelectedPublicationMissing =
- isSuccessPublications && !!publicationName && !publicationNames.includes(publicationName)
- const allValidationFailures = [...destinationValidationFailures, ...pipelineValidationFailures]
- const hasValidationFailures = allValidationFailures.some((f) => f.failure_type === 'critical')
- const isSaving =
- creatingDestinationPipeline ||
- updatingDestinationPipeline ||
- startingPipeline ||
- isCreatingS3AccessKey ||
- isCreatingNamespace ||
- isValidating
- const isSubmitDisabled =
- isSaving || isSelectedPublicationMissing || (!editMode && hasNoAvailableDestinations)
- const getSubmitButtonText = () => {
- if (editMode) {
- return existingDestination?.enabled ? 'Apply and restart' : 'Apply and start'
- } else {
- return 'Create and start'
- }
- }
- // Helper function to handle namespace creation if needed
- const resolveNamespace = async (data: z.infer<typeof FormSchema>) => {
- if (data.namespace === CREATE_NEW_NAMESPACE) {
- if (!data.newNamespaceName) throw new Error('New namespace name is required')
- await createNamespace({
- projectRef,
- warehouse: data.warehouseName!,
- namespace: data.newNamespaceName,
- })
- return data.newNamespaceName
- }
- return data.namespace
- }
- // Helper function to validate configuration
- const validateConfiguration = async (data: z.infer<typeof FormSchema>) => {
- if (!projectRef || !sourceId) return false
- setHasRunValidation(true)
- // Call both validation endpoints in parallel and wait for both to complete
- // even if one fails - this makes the validation feel like a single operation
- const results = await Promise.allSettled([
- validateDestination({
- projectRef,
- destinationConfig: buildDestinationConfigForValidation({ projectRef, selectedType, data }),
- }),
- validatePipeline({
- projectRef,
- sourceId,
- publicationName: data.publicationName,
- maxFillMs: data.maxFillMs,
- maxTableSyncWorkers: data.maxTableSyncWorkers,
- maxCopyConnectionsPerTable: data.maxCopyConnectionsPerTable,
- invalidatedSlotBehavior: data.invalidatedSlotBehavior,
- }),
- ])
- // Extract results from settled promises
- const destResult = results[0]
- const pipelineResult = results[1]
- // Check if any validation request failed completely
- const hasRequestError = results.some((r) => r.status === 'rejected')
- if (hasRequestError) {
- // If any request failed, surface the upstream message so users see why
- const rejected = results.find((r): r is PromiseRejectedResult => r.status === 'rejected')
- const reason =
- rejected?.reason instanceof Error ? rejected.reason.message : 'Please try again.'
- toast.error(`Failed to validate configuration: ${reason}`)
- setHasRunValidation(false)
- return false
- }
- // Both requests succeeded, extract validation failures
- const destValidationResult =
- destResult.status === 'fulfilled' ? destResult.value : { validation_failures: [] }
- const pipelineValidationResult =
- pipelineResult.status === 'fulfilled' ? pipelineResult.value : { validation_failures: [] }
- setDestinationValidationFailures(destValidationResult.validation_failures)
- setPipelineValidationFailures(pipelineValidationResult.validation_failures)
- // Check if there are critical failures or warnings
- const allFailures = [
- ...destValidationResult.validation_failures,
- ...pipelineValidationResult.validation_failures,
- ]
- const hasCriticalFailures = allFailures.some((f) => f.failure_type === 'critical')
- const hasAnyFailures = allFailures.length > 0
- // Scroll to validation section if there are any failures
- if (hasAnyFailures) {
- setTimeout(() => {
- validationSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' })
- }, 100)
- }
- return !hasCriticalFailures
- }
- const submitPipeline = async (data: z.infer<typeof FormSchema>) => {
- if (!projectRef) return console.error('Project ref is required')
- if (!sourceId) return console.error('Source id is required')
- if (isSelectedPublicationMissing) {
- return toast.error('Please select another publication before continuing')
- }
- try {
- const destinationConfig = await buildDestinationConfig({
- projectRef,
- selectedType,
- warehouseName,
- data,
- createS3AccessKey,
- resolveNamespace,
- })
- if (!destinationConfig) throw new Error('Destination configuration is missing')
- const batchConfig: BatchConfig | undefined =
- data.maxFillMs !== undefined ? { maxFillMs: data.maxFillMs } : undefined
- const hasBatchFields = batchConfig !== undefined
- const pipelineConfig = {
- publicationName: data.publicationName,
- maxTableSyncWorkers: data.maxTableSyncWorkers,
- maxCopyConnectionsPerTable: data.maxCopyConnectionsPerTable,
- invalidatedSlotBehavior: data.invalidatedSlotBehavior,
- ...(hasBatchFields ? { batch: batchConfig } : {}),
- }
- if (editMode && existingDestination) {
- if (!existingDestination.pipelineId) return console.error('Pipeline id is required')
- await updateDestinationPipeline({
- destinationId: existingDestination.destinationId,
- pipelineId: existingDestination.pipelineId,
- projectRef,
- destinationName: data.name,
- destinationConfig,
- pipelineConfig,
- sourceId,
- })
- // Set request status only right before starting, then fire and close
- const snapshot =
- existingDestination.statusName ?? (existingDestination.enabled ? 'started' : 'stopped')
- if (existingDestination.enabled) {
- setRequestStatus(
- existingDestination.pipelineId,
- PipelineStatusRequestStatus.RestartRequested,
- snapshot
- )
- toast.success('Settings applied. Restarting the pipeline...')
- restartPipeline({ projectRef, pipelineId: existingDestination.pipelineId })
- } else {
- setRequestStatus(
- existingDestination.pipelineId,
- PipelineStatusRequestStatus.StartRequested,
- snapshot
- )
- toast.success('Settings applied. Starting the pipeline...')
- startPipeline({ projectRef, pipelineId: existingDestination.pipelineId })
- }
- onClose()
- } else {
- const { pipeline_id: pipelineId } = await createDestinationPipeline({
- projectRef,
- destinationName: data.name,
- destinationConfig,
- pipelineConfig,
- sourceId,
- })
- // Set request status only right before starting, then fire and close
- setRequestStatus(pipelineId, PipelineStatusRequestStatus.StartRequested, undefined)
- toast.success('Destination created. Starting the pipeline...')
- startPipeline({ projectRef, pipelineId })
- onClose()
- }
- } catch (error) {
- const action = editMode ? 'apply and run' : 'create and start'
- toast.error(`Failed to ${action} destination: ${(error as ResponseError).message}`)
- }
- }
- const onSubmit = async (data: z.infer<typeof FormSchema>) => {
- if (!editMode) {
- // For new pipelines, validate configuration first if not already validated
- // OR if user has critical failures and clicks "Validate again"
- if (!hasRunValidation || isValidating || hasValidationFailures) {
- const isValid = await validateConfiguration(data)
- if (!isValid) {
- // Validation failed with critical errors, show inline and stop
- return
- }
- // Validation passed or only has warnings, continue to disclaimer
- }
- // Validation passed or only warnings, proceed to disclaimer
- setPendingFormValues(data)
- setShowDisclaimerDialog(true)
- return
- }
- await submitPipeline(data)
- }
- const handleDisclaimerDialogChange = (open: boolean) => {
- setShowDisclaimerDialog(open)
- if (!open) {
- setPendingFormValues(null)
- }
- }
- const handleDisclaimerConfirm = async () => {
- if (!pendingFormValues) return
- const values = pendingFormValues
- setPendingFormValues(null)
- setShowDisclaimerDialog(false)
- await submitPipeline(values)
- }
- useEffect(() => {
- if (editMode && destinationData && pipelineData && !isFormInteracting) {
- form.reset(defaultValues)
- }
- }, [destinationData, pipelineData, editMode, defaultValues, form, isFormInteracting])
- // Ensure the form always reflects the freshest data whenever the panel opens
- useEffect(() => {
- if (visible) {
- form.reset(defaultValues)
- setIsFormInteracting(false)
- setHasRunValidation(false)
- setDestinationValidationFailures([])
- setPipelineValidationFailures([])
- }
- }, [visible, defaultValues, form])
- useEffect(() => {
- if (visible && projectRef && sourceId) {
- refetchPublications()
- }
- }, [visible, projectRef, sourceId, refetchPublications])
- return (
- <>
- <SheetSection className="grow overflow-auto px-0 py-0">
- {hasNoAvailableDestinations && !editMode ? (
- <NoDestinationsAvailable />
- ) : (
- <Form {...form}>
- <form id={formId} onSubmit={form.handleSubmit(onSubmit)}>
- <div className="p-5 flex flex-col gap-y-6">
- <p className="text-sm font-medium text-foreground">Destination details</p>
- <div className="space-y-4">
- <DestinationNameInput form={form} />
- <PublicationSelection
- form={form}
- sourceId={sourceId}
- visible={visible}
- onSelectNewPublication={() => setPublicationPanelVisible(true)}
- />
- </div>
- </div>
- <DialogSectionSeparator />
- {selectedType === 'BigQuery' && etlEnableBigQuery ? (
- <BigQueryFields form={form} />
- ) : selectedType === 'Analytics Bucket' && etlEnableIceberg ? (
- <AnalyticsBucketFields
- form={form}
- setIsFormInteracting={setIsFormInteracting}
- onSelectNewBucket={() => setNewBucketSheetVisible(true)}
- />
- ) : selectedType === 'DuckLake' && etlEnableDucklake ? (
- <DuckLakeFields form={form} />
- ) : null}
- <DialogSectionSeparator />
- <AdvancedSettings type={selectedType} form={form} />
- {!editMode && hasRunValidation && !isValidating && (
- <>
- <DialogSectionSeparator />
- <div ref={validationSectionRef}>
- <ValidationFailuresSection
- destinationFailures={destinationValidationFailures}
- pipelineFailures={pipelineValidationFailures}
- />
- </div>
- </>
- )}
- </form>
- </Form>
- )}
- </SheetSection>
- <SheetFooter className="justify-between!">
- <AnimatePresence mode="wait">
- {isValidating || isSaving ? (
- <motion.div
- className="flex items-center gap-x-2"
- initial={{ opacity: 0, y: 5 }}
- animate={{ opacity: 1, y: 0 }}
- exit={{ opacity: 0, y: 5 }}
- transition={{ duration: 0.2, ease: 'easeOut' }}
- >
- <Loader2 className="animate-spin" size={14} />
- <p className="text-foreground-light text-sm">
- {isValidating
- ? 'Validating destination configuration...'
- : `${editMode ? 'Updating' : 'Creating'} destination...`}
- </p>
- </motion.div>
- ) : (
- <div />
- )}
- </AnimatePresence>
- <div className="flex items-center gap-x-2">
- <Button disabled={isSaving} type="default" onClick={onClose}>
- Cancel
- </Button>
- <Button disabled={isSubmitDisabled} loading={isSaving} form={formId} htmlType="submit">
- {getSubmitButtonText()}
- </Button>
- </div>
- </SheetFooter>
- <NewPublicationPanel
- sourceId={sourceId}
- visible={publicationPanelVisible}
- onClose={() => setPublicationPanelVisible(false)}
- />
- <CreateAnalyticsBucketSheet
- open={newBucketSheetVisible}
- onOpenChange={setNewBucketSheetVisible}
- />
- <ReplicationDisclaimerDialog
- open={showDisclaimerDialog}
- onOpenChange={handleDisclaimerDialogChange}
- isLoading={isSaving}
- onConfirm={handleDisclaimerConfirm}
- />
- </>
- )
- }
|