diff --git a/apps/studio/components/interfaces/Database/Replication/CreatePipeline/PipelineDataSection.test.tsx b/apps/studio/components/interfaces/Database/Replication/CreatePipeline/PipelineDataSection.test.tsx new file mode 100644 index 00000000000..90e69d6aebf --- /dev/null +++ b/apps/studio/components/interfaces/Database/Replication/CreatePipeline/PipelineDataSection.test.tsx @@ -0,0 +1,119 @@ +import { fireEvent, screen, waitFor } from '@testing-library/react' +import type { components } from 'api-types' +import { mockAnimationsApi } from 'jsdom-testing-mocks' +import { HttpResponse } from 'msw' +import { useForm } from 'react-hook-form' +import { Form } from 'ui' +import { describe, expect, it, vi } from 'vitest' + +import type { DestinationPanelSchemaType } from '../DestinationPanel/DestinationForm/DestinationForm.schema' +import type { DestinationType } from '../DestinationPanel/DestinationPanel.types' +import { PipelineDataSection } from './PipelineDataSection' +import { customRender } from '@/tests/lib/custom-render' +import { addAPIMock } from '@/tests/lib/msw' + +mockAnimationsApi() +type ReplicationSourcesResponse = components['schemas']['SourcesResponse_Output'] +type PublicationDetailsResponse = components['schemas']['PublicationDetailsResponse_Output'] +type PublicationNamesResponse = components['schemas']['ReadPublicationsResponse_Output'] + +const mockSources: ReplicationSourcesResponse = { + sources: [ + { + id: 1, + name: 'default', + tenant_id: 'tenant', + config: { host: 'db.internal', name: 'main-db', port: 5432, username: 'etl_user' }, + }, + ], +} + +const mockPublicationDetails = (publishViaPartitionRoot: boolean): PublicationDetailsResponse => ({ + name: 'analytics', + config: { + type: 'tables', + tables: [], + operations: ['insert', 'update', 'delete', 'truncate'], + publish_via_partition_root: publishViaPartitionRoot, + }, + tables: [], +}) + +const mockPublicationRequests = (publishViaPartitionRoot: boolean) => { + addAPIMock({ + method: 'get', + path: '/platform/replication/:ref/sources', + response: () => HttpResponse.json(mockSources), + }) + addAPIMock({ + method: 'get', + path: '/platform/replication/v2/:ref/sources/:source_id/publications', + response: () => + HttpResponse.json({ + publications: [{ name: 'analytics' }], + }), + }) + addAPIMock({ + method: 'get', + path: '/platform/replication/v2/:ref/sources/:source_id/publications/:publication_name', + response: () => + HttpResponse.json( + mockPublicationDetails(publishViaPartitionRoot) + ), + }) +} + +const TestSection = ({ type }: { type: DestinationType }) => { + const form = useForm({ + defaultValues: { + publicationName: 'analytics', + tableSyncCopyMode: 'include_all_tables', + tableSyncCopyTableIds: [], + maxFillMs: 500, + maxTableSyncWorkers: 4, + maxCopyConnectionsPerTable: 1, + invalidatedSlotBehavior: 'error', + tableOptions: [], + }, + }) + return ( +
+ + + ) +} + +describe('PipelineDataSection', () => { + it.each(['BigQuery', 'DuckLake', 'Snowflake', 'ClickHouse', 'Analytics Bucket'])( + 'groups %s publication, initial sync and replication settings without connection fields', + async (type) => { + mockPublicationRequests(false) + customRender() + expect(screen.getByRole('region', { name: 'Data' })).toBeInTheDocument() + expect(screen.getByText('Initial sync')).toBeInTheDocument() + await waitFor(() => + expect(screen.getByRole('status')).toHaveTextContent( + 'Each partition is replicated separately.' + ) + ) + fireEvent.click(screen.getByRole('button', { name: /Advanced settings/ })) + expect(screen.getByLabelText('Batch wait time')).toHaveValue(500) + expect(screen.getByLabelText('Table sync workers')).toHaveValue(4) + expect(screen.queryByText('Table layout') !== null).toBe(type === 'BigQuery') + for (const label of [ + 'Pipeline name', + 'Pipeline region', + 'Connection pool size', + 'Maximum staleness', + 'Pool size', + 'Role', + ]) { + expect(screen.queryByText(label)).not.toBeInTheDocument() + } + fireEvent.change(screen.getByLabelText('Batch wait time'), { target: { value: '750' } }) + fireEvent.click(screen.getByRole('button', { name: /Advanced settings/ })) + fireEvent.click(screen.getByRole('button', { name: /Advanced settings/ })) + expect(screen.getByLabelText('Batch wait time')).toHaveValue(750) + } + ) +}) diff --git a/apps/studio/components/interfaces/Database/Replication/CreatePipeline/PipelineDataSection.tsx b/apps/studio/components/interfaces/Database/Replication/CreatePipeline/PipelineDataSection.tsx new file mode 100644 index 00000000000..934ec566f3f --- /dev/null +++ b/apps/studio/components/interfaces/Database/Replication/CreatePipeline/PipelineDataSection.tsx @@ -0,0 +1,28 @@ +import type { UseFormReturn } from 'react-hook-form' + +import { AdvancedSettings } from '../DestinationPanel/DestinationForm/AdvancedSettings' +import type { DestinationPanelSchemaType } from '../DestinationPanel/DestinationForm/DestinationForm.schema' +import { PublicationSelection } from '../DestinationPanel/DestinationForm/PublicationSelection' +import { TableCopySelection } from '../DestinationPanel/DestinationForm/TableCopySelection' +import type { DestinationType } from '../DestinationPanel/DestinationPanel.types' + +export const PipelineDataSection = ({ + type, + form, + onSelectNewPublication, +}: { + type: DestinationType + form: UseFormReturn + onSelectNewPublication: () => void +}) => ( +
+
+

Data

+
+ + +
+
+ +
+) diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.test.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.test.tsx index 254ee8371c9..bbfcd09a7c6 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.test.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.test.tsx @@ -317,6 +317,7 @@ describe('DestinationForm edit submission', () => { onClose={vi.fn()} /> ) + expect(screen.queryByRole('region', { name: 'Data' }) !== null).toBe(shouldShowSection) expect(screen.queryByRole('region', { name: 'Destination connection' }) !== null).toBe( shouldShowSection ) diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx index b260639232c..88d57dedf69 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/index.tsx @@ -11,6 +11,7 @@ import { Admonition } from 'ui-patterns/Admonition' import * as z from 'zod' import { PipelineConnectionSection } from '../../CreatePipeline/PipelineConnectionSection' +import { PipelineDataSection } from '../../CreatePipeline/PipelineDataSection' import { useIsETLBigQueryPrivateAlpha, useIsETLClickHousePrivateAlpha, @@ -497,49 +498,45 @@ export const DestinationForm = ({ )} -
- {isErrorExistingConfig && ( - -

- The existing destination or pipeline settings could not be loaded. Refresh - before applying changes. -

-
- )} -

- {shouldShowConnectionSection ? 'Data' : 'Destination details'} -

- -
- {!shouldShowConnectionSection && ( - - )} - setPublicationPanelVisible(true)} - /> - - {!shouldShowConnectionSection && ( - - )} -
-
- - {!shouldShowConnectionSection && ( + {shouldShowConnectionSection ? ( + setPublicationPanelVisible(true)} + /> + ) : ( <> +
+ {isErrorExistingConfig && ( + +

+ The existing destination or pipeline settings could not be loaded. + Refresh before applying changes. +

+
+ )} +

Destination details

+ +
+ + setPublicationPanelVisible(true)} + /> + + +
+
+ {destinationFields} + + + + )} - {!shouldShowConnectionSection && } - - - {!editMode && hasRunValidation && !isValidating && ( <>