mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 06:09:08 +03:00
refactor(studio): compose the pipeline data section for wizard reuse (#51572)
## Problem The pipeline wizard in #49243 needs publication, initial sync and replication settings together on its data step. ## Solution Compose `PipelineDataSection` from the existing controls and use it in preview-enabled creation. It includes the publication selector, initial-sync controls and data advanced settings, including BigQuery table layout. This preserves the grouping from #51570. Preview off and editing keep the existing layout. Validation and creation behaviour stay the same; no wizard routing is added. Stacked on #51570. ## Review instructions 1. Open the [Pipelines preview](https://studio-staging-git-dnywh-featpipeline-data-section-supabase.vercel.app/dashboard/project/_/database/pipelines), select a test project with Pipelines enabled and go to **Database → Pipelines**. Enable **Pipeline creation wizard** in Feature Previews, then click **Add pipeline**. Confirm **Data** follows **Destination connection**, with publication and initial sync above its **Advanced settings**. 2. Select a publication and change the initial-sync option. For selected-table options, confirm the publication tables are available. Click **New publication** in the publication selector and confirm the existing creation panel opens, then cancel it. 3. Expand **Advanced settings** under **Data**. Confirm replication settings appear, plus table layout for BigQuery, without connection pool size, staleness or role. Change **Batch wait time**, collapse and reopen the accordion, and confirm the value remains. Check desktop and phone widths. Cancel and discard changes; no credentials or pipeline creation are needed. 4. Turn the preview off and reopen **Add pipeline**. Confirm the original layout remains. With the preview on, edit an existing pipeline and confirm its original layout also remains.
This commit is contained in:
1 parent
37fb536e6c
commit
7fa436050a
4 files changed
+182
-37
No files matched your search
+119
@@ -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<ReplicationSourcesResponse>(mockSources),
|
||||
})
|
||||
addAPIMock({
|
||||
method: 'get',
|
||||
path: '/platform/replication/v2/:ref/sources/:source_id/publications',
|
||||
response: () =>
|
||||
HttpResponse.json<PublicationNamesResponse>({
|
||||
publications: [{ name: 'analytics' }],
|
||||
}),
|
||||
})
|
||||
addAPIMock({
|
||||
method: 'get',
|
||||
path: '/platform/replication/v2/:ref/sources/:source_id/publications/:publication_name',
|
||||
response: () =>
|
||||
HttpResponse.json<PublicationDetailsResponse>(
|
||||
mockPublicationDetails(publishViaPartitionRoot)
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
const TestSection = ({ type }: { type: DestinationType }) => {
|
||||
const form = useForm<DestinationPanelSchemaType>({
|
||||
defaultValues: {
|
||||
publicationName: 'analytics',
|
||||
tableSyncCopyMode: 'include_all_tables',
|
||||
tableSyncCopyTableIds: [],
|
||||
maxFillMs: 500,
|
||||
maxTableSyncWorkers: 4,
|
||||
maxCopyConnectionsPerTable: 1,
|
||||
invalidatedSlotBehavior: 'error',
|
||||
tableOptions: [],
|
||||
},
|
||||
})
|
||||
return (
|
||||
<Form {...form}>
|
||||
<PipelineDataSection type={type} form={form} onSelectNewPublication={vi.fn()} />
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
describe('PipelineDataSection', () => {
|
||||
it.each<DestinationType>(['BigQuery', 'DuckLake', 'Snowflake', 'ClickHouse', 'Analytics Bucket'])(
|
||||
'groups %s publication, initial sync and replication settings without connection fields',
|
||||
async (type) => {
|
||||
mockPublicationRequests(false)
|
||||
customRender(<TestSection type={type} />)
|
||||
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)
|
||||
}
|
||||
)
|
||||
})
|
||||
+28
@@ -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<DestinationPanelSchemaType>
|
||||
onSelectNewPublication: () => void
|
||||
}) => (
|
||||
<section aria-label="Data">
|
||||
<div className="p-5 flex flex-col gap-y-6">
|
||||
<h3 className="text-sm font-medium text-foreground">Data</h3>
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<PublicationSelection form={form} onSelectNewPublication={onSelectNewPublication} />
|
||||
<TableCopySelection form={form} editMode={false} />
|
||||
</div>
|
||||
</div>
|
||||
<AdvancedSettings type={type} form={form} group="data" />
|
||||
</section>
|
||||
)
|
||||
+1
@@ -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
|
||||
)
|
||||
|
||||
+34
-37
@@ -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 = ({
|
||||
<DialogSectionSeparator />
|
||||
</>
|
||||
)}
|
||||
<div className="p-5 flex flex-col gap-y-6">
|
||||
{isErrorExistingConfig && (
|
||||
<Admonition type="warning">
|
||||
<p className="leading-normal!">
|
||||
The existing destination or pipeline settings could not be loaded. Refresh
|
||||
before applying changes.
|
||||
</p>
|
||||
</Admonition>
|
||||
)}
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
{shouldShowConnectionSection ? 'Data' : 'Destination details'}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col gap-y-4">
|
||||
{!shouldShowConnectionSection && (
|
||||
<DestinationNameInput form={form} destinationType={selectedType} />
|
||||
)}
|
||||
<PublicationSelection
|
||||
form={form}
|
||||
onSelectNewPublication={() => setPublicationPanelVisible(true)}
|
||||
/>
|
||||
<TableCopySelection form={form} editMode={editMode} />
|
||||
{!shouldShowConnectionSection && (
|
||||
<PipelineRegionField destinationType={selectedType} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!shouldShowConnectionSection && (
|
||||
{shouldShowConnectionSection ? (
|
||||
<PipelineDataSection
|
||||
type={selectedType}
|
||||
form={form}
|
||||
onSelectNewPublication={() => setPublicationPanelVisible(true)}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="p-5 flex flex-col gap-y-6">
|
||||
{isErrorExistingConfig && (
|
||||
<Admonition type="warning">
|
||||
<p className="leading-normal!">
|
||||
The existing destination or pipeline settings could not be loaded.
|
||||
Refresh before applying changes.
|
||||
</p>
|
||||
</Admonition>
|
||||
)}
|
||||
<p className="text-sm font-medium text-foreground">Destination details</p>
|
||||
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<DestinationNameInput form={form} destinationType={selectedType} />
|
||||
<PublicationSelection
|
||||
form={form}
|
||||
onSelectNewPublication={() => setPublicationPanelVisible(true)}
|
||||
/>
|
||||
<TableCopySelection form={form} editMode={editMode} />
|
||||
<PipelineRegionField destinationType={selectedType} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogSectionSeparator />
|
||||
{destinationFields}
|
||||
|
||||
<DialogSectionSeparator />
|
||||
|
||||
<AdvancedSettings type={selectedType} form={form} group="all" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{!shouldShowConnectionSection && <DialogSectionSeparator />}
|
||||
|
||||
<AdvancedSettings
|
||||
type={selectedType}
|
||||
form={form}
|
||||
group={shouldShowConnectionSection ? 'data' : 'all'}
|
||||
/>
|
||||
|
||||
{!editMode && hasRunValidation && !isValidating && (
|
||||
<>
|
||||
<DialogSectionSeparator />
|
||||
|
||||
Reference in new issue
Block a user