diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel.tsx index 2d4aea160f..36b0407e88 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel.tsx @@ -6,7 +6,6 @@ import * as z from 'zod' import { useParams } from 'common' import { useCreateDestinationPipelineMutation } from 'data/replication/create-destination-pipeline-mutation' -import { useCreateTenantSourceMutation } from 'data/replication/create-tenant-source-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' @@ -21,9 +20,6 @@ import { AccordionContent_Shadcn_, AccordionItem_Shadcn_, AccordionTrigger_Shadcn_, - Alert_Shadcn_, - AlertDescription_Shadcn_, - AlertTitle_Shadcn_, Button, Form_Shadcn_, FormControl_Shadcn_, @@ -43,7 +39,6 @@ import { SheetSection, SheetTitle, TextArea_Shadcn_, - WarningIcon, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import NewPublicationPanel from './NewPublicationPanel' @@ -60,7 +55,6 @@ const FormSchema = z.object({ datasetId: z.string().min(1, 'Dataset id is required'), serviceAccountKey: z.string().min(1, 'Service account key is required'), publicationName: z.string().min(1, 'Publication is required'), - maxSize: z.number().min(1, 'Max Size must be greater than 0').int().optional(), maxFillMs: z.number().min(1, 'Max Fill milliseconds should be greater than 0').int().optional(), maxStalenessMins: z.number().nonnegative().optional(), }) @@ -90,9 +84,6 @@ export const DestinationPanel = ({ const editMode = !!existingDestination const [publicationPanelVisible, setPublicationPanelVisible] = useState(false) - const { mutateAsync: createTenantSource, isLoading: creatingTenantSource } = - useCreateTenantSourceMutation() - const { mutateAsync: createDestinationPipeline, isLoading: creatingDestinationPipeline } = useCreateDestinationPipelineMutation({ onSuccess: () => form.reset(defaultValues), @@ -129,7 +120,6 @@ export const DestinationPanel = ({ // For now, the password will always be set as empty for security reasons. serviceAccountKey: destinationData?.config?.big_query?.service_account_key ?? '', publicationName: pipelineData?.config.publication_name ?? '', - maxSize: pipelineData?.config?.batch?.max_size, maxFillMs: pipelineData?.config?.batch?.max_fill_ms, maxStalenessMins: destinationData?.config?.big_query?.max_staleness_mins, }), @@ -162,9 +152,8 @@ export const DestinationPanel = ({ } const batchConfig: any = {} - if (!!data.maxSize) batchConfig.maxSize = data.maxSize if (!!data.maxFillMs) batchConfig.maxFillMs = data.maxFillMs - const hasBothBatchFields = Object.keys(batchConfig).length === 2 + const hasBatchFields = Object.keys(batchConfig).length > 0 await updateDestinationPipeline({ destinationId: existingDestination.destinationId, @@ -174,7 +163,7 @@ export const DestinationPanel = ({ destinationConfig: { bigQuery: bigQueryConfig }, pipelineConfig: { publicationName: data.publicationName, - ...(hasBothBatchFields ? { batch: batchConfig } : {}), + ...(hasBatchFields ? { batch: batchConfig } : {}), }, sourceId, }) @@ -209,9 +198,8 @@ export const DestinationPanel = ({ } const batchConfig: any = {} - if (!!data.maxSize) batchConfig.maxSize = data.maxSize if (!!data.maxFillMs) batchConfig.maxFillMs = data.maxFillMs - const hasBothBatchFields = Object.keys(batchConfig).length === 2 + const hasBatchFields = Object.keys(batchConfig).length > 0 const { pipeline_id: pipelineId } = await createDestinationPipeline({ projectRef, @@ -220,7 +208,7 @@ export const DestinationPanel = ({ sourceId, pipelineConfig: { publicationName: data.publicationName, - ...(hasBothBatchFields ? { batch: batchConfig } : {}), + ...(hasBatchFields ? { batch: batchConfig } : {}), }, }) // Set request status only right before starting, then fire and close @@ -235,11 +223,6 @@ export const DestinationPanel = ({ } } - const onEnableReplication = async () => { - if (!projectRef) return console.error('Project ref is required') - await createTenantSource({ projectRef }) - } - useEffect(() => { if (editMode && destinationData && pipelineData) { form.reset(defaultValues) @@ -253,7 +236,7 @@ export const DestinationPanel = ({ } }, [visible, defaultValues, form]) - return sourceId ? ( + return ( <> @@ -399,31 +382,6 @@ export const DestinationPanel = ({ Advanced Settings - ( - - - { - const val = e.target.value - field.onChange(val === '' ? undefined : Number(val)) - }} - placeholder="Leave empty for default" - /> - - - )} - /> + setPublicationPanelVisible(false)} /> - ) : ( - - -
- - Create a new destination - - - - - - {/* Pricing to be decided yet */} - Enabling replication will cost additional $xx.xx - - - -
- -
-
-
-
- - - -
-
-
) } diff --git a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx index c1c133f3f0..604b908c1d 100644 --- a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx +++ b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx @@ -1,19 +1,21 @@ +import { useQueryClient } from '@tanstack/react-query' import { Plus, Search } from 'lucide-react' import { useEffect, useRef, useState } from 'react' import { useParams } from 'common' import Table from 'components/to-be-cleaned/Table' -import AlertError from 'components/ui/AlertError' +import { AlertError } from 'components/ui/AlertError' +import { DocsButton } from 'components/ui/DocsButton' import { useReplicationDestinationsQuery } from 'data/replication/destinations-query' +import { replicationKeys } from 'data/replication/keys' +import { fetchReplicationPipelineVersion } from 'data/replication/pipeline-version-query' import { useReplicationPipelinesQuery } from 'data/replication/pipelines-query' import { useReplicationSourcesQuery } from 'data/replication/sources-query' -import { fetchReplicationPipelineVersion } from 'data/replication/pipeline-version-query' -import { replicationKeys } from 'data/replication/keys' -import { useQueryClient } from '@tanstack/react-query' import { Button, cn, Input_Shadcn_ } from 'ui' import { GenericSkeletonLoader } from 'ui-patterns' import { DestinationPanel } from './DestinationPanel' import { DestinationRow } from './DestinationRow' +import { EnableReplicationModal } from './EnableReplicationModal' import { PIPELINE_ERROR_MESSAGES } from './Pipeline.utils' export const Destinations = () => { @@ -26,11 +28,13 @@ export const Destinations = () => { error: sourcesError, isLoading: isSourcesLoading, isError: isSourcesError, + isSuccess: isSourcesSuccess, } = useReplicationSourcesQuery({ projectRef, }) const sourceId = sourcesData?.sources.find((s) => s.name === projectRef)?.id + const replicationNotEnabled = isSourcesSuccess && !sourceId const { data: destinationsData, @@ -52,7 +56,7 @@ export const Destinations = () => { projectRef, }) - const anyDestinations = isDestinationsSuccess && destinationsData.destinations.length > 0 + const hasDestinations = isDestinationsSuccess && destinationsData.destinations.length > 0 const filteredDestinations = filterString.length === 0 @@ -103,9 +107,11 @@ export const Destinations = () => { /> - + {!!sourceId && ( + + )} @@ -119,7 +125,21 @@ export const Destinations = () => { /> )} - {anyDestinations ? ( + {replicationNotEnabled ? ( +
+
+

Run analysis on your data via integrations with Replication

+

+ Enable replication on your project to send data to your first destination +

+
+
+ + {/* [Joshen] Placeholder for when we have documentation */} + +
+
+ ) : hasDestinations ? ( Name, @@ -147,7 +167,7 @@ export const Destinations = () => { /> ) })} - >
+ /> ) : ( !isSourcesLoading && !isDestinationsLoading && @@ -180,7 +200,7 @@ export const Destinations = () => { {!isSourcesLoading && !isDestinationsLoading && filteredDestinations.length === 0 && - anyDestinations && ( + hasDestinations && (

No destinations match "{filterString}"

diff --git a/apps/studio/components/interfaces/Database/Replication/EnableReplicationModal.tsx b/apps/studio/components/interfaces/Database/Replication/EnableReplicationModal.tsx new file mode 100644 index 0000000000..c84b826ab4 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Replication/EnableReplicationModal.tsx @@ -0,0 +1,78 @@ +import { useState } from 'react' +import { toast } from 'sonner' + +import { useParams } from 'common' +import { useCreateTenantSourceMutation } from 'data/replication/create-tenant-source-mutation' +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogSection, + DialogSectionSeparator, + DialogTitle, + DialogTrigger, +} from 'ui' +import { Admonition } from 'ui-patterns' + +export const EnableReplicationModal = () => { + const { ref: projectRef } = useParams() + const [open, setOpen] = useState(false) + + const { mutateAsync: createTenantSource, isLoading: creatingTenantSource } = + useCreateTenantSourceMutation({ + onSuccess: () => { + toast.success('Replication has been successfully enabled!') + setOpen(false) + }, + onError: (error) => { + toast.error(`Failed to enable replication: ${error.message}`) + }, + }) + + const onEnableReplication = async () => { + if (!projectRef) return console.error('Project ref is required') + await createTenantSource({ projectRef }) + } + + return ( + + + + + + + Confirm to enable Replication + + + + +

+ This feature is in active development and may change as we gather feedback. + Availability and behavior can evolve while in Alpha. +

+

+ Pricing has not been finalized yet. You can enable replication now; we’ll announce + pricing later and notify you before any charges apply. +

+
+
+ + + + +
+
+ ) +} diff --git a/apps/studio/components/ui/AlertError.tsx b/apps/studio/components/ui/AlertError.tsx index 5081f7385f..117b051c8e 100644 --- a/apps/studio/components/ui/AlertError.tsx +++ b/apps/studio/components/ui/AlertError.tsx @@ -19,7 +19,7 @@ export interface AlertErrorProps { // [Joshen] To standardize the language for all error UIs -const AlertError = ({ +export const AlertError = ({ projectRef, subject, error, diff --git a/apps/studio/data/replication/create-destination-pipeline-mutation.ts b/apps/studio/data/replication/create-destination-pipeline-mutation.ts index ce9e285b09..2189dd8cd0 100644 --- a/apps/studio/data/replication/create-destination-pipeline-mutation.ts +++ b/apps/studio/data/replication/create-destination-pipeline-mutation.ts @@ -22,7 +22,6 @@ export type CreateDestinationPipelineParams = { pipelineConfig: { publicationName: string batch?: { - maxSize: number maxFillMs: number } } @@ -60,7 +59,6 @@ async function createDestinationPipeline( ...(batch ? { batch: { - max_size: batch.maxSize, max_fill_ms: batch.maxFillMs, }, } diff --git a/apps/studio/data/replication/update-destination-pipeline-mutation.ts b/apps/studio/data/replication/update-destination-pipeline-mutation.ts index c96719e1df..2bd6830a8e 100644 --- a/apps/studio/data/replication/update-destination-pipeline-mutation.ts +++ b/apps/studio/data/replication/update-destination-pipeline-mutation.ts @@ -24,7 +24,6 @@ export type UpdateDestinationPipelineParams = { pipelineConfig: { publicationName: string batch?: { - maxSize: number maxFillMs: number } } @@ -64,7 +63,6 @@ async function updateDestinationPipeline( publication_name: publicationName, ...(batch && { batch: { - max_size: batch.maxSize, max_fill_ms: batch.maxFillMs, }, }), diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts index 7204239e4a..525b4f2561 100644 --- a/packages/api-types/types/platform.d.ts +++ b/packages/api-types/types/platform.d.ts @@ -5142,12 +5142,7 @@ export interface components { * @description Maximum fill time in milliseconds * @example 200 */ - max_fill_ms: number - /** - * @description Maximum batch size - * @example 5000 - */ - max_size: number + max_fill_ms?: number } /** * @description Publication name @@ -5170,12 +5165,7 @@ export interface components { * @description Maximum fill time in milliseconds * @example 200 */ - max_fill_ms: number - /** - * @description Maximum batch size - * @example 5000 - */ - max_size: number + max_fill_ms?: number } /** * @description Publication name @@ -8015,12 +8005,7 @@ export interface components { * @description Maximum fill time in milliseconds * @example 200 */ - max_fill_ms: number - /** - * @description Maximum batch size - * @example 5000 - */ - max_size: number + max_fill_ms?: number } /** * @description Publication name @@ -8075,12 +8060,7 @@ export interface components { * @description Maximum fill time in milliseconds * @example 200 */ - max_fill_ms: number - /** - * @description Maximum batch size - * @example 5000 - */ - max_size: number + max_fill_ms?: number } /** * @description Publication name @@ -9429,12 +9409,7 @@ export interface components { * @description Maximum fill time in milliseconds * @example 200 */ - max_fill_ms: number - /** - * @description Maximum batch size - * @example 5000 - */ - max_size: number + max_fill_ms?: number } /** * @description Publication name @@ -9457,12 +9432,7 @@ export interface components { * @description Maximum fill time in milliseconds * @example 200 */ - max_fill_ms: number - /** - * @description Maximum batch size - * @example 5000 - */ - max_size: number + max_fill_ms?: number } /** * @description Publication name