mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
## Problem Replicated tables on the pipeline detail page reshuffle after a reset. API order is unstable, and the default client sort was by status, so status changes moved rows around. Fixes PIPE-1137. ## Solution Default the replicated tables table to sort by `schema` + `name` ascending. Status sorting remains available via the Status column header. ## To test 1. Open a project with a pipeline that has several replicated tables: `/project/<ref>/database/replication/<pipelineId>`. 2. Confirm the Replicated tables list is alphabetical by `schema.name`, with the Table column showing ascending sort. 3. Reset one table and confirm its row stays in place while its status updates. 4. Click the Status column header and confirm status sorting still works. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Improvements** * Replicated tables are now sorted alphabetically by schema and table name by default. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
190 lines
6.1 KiB
TypeScript
190 lines
6.1 KiB
TypeScript
import { screen, within } from '@testing-library/react'
|
|
import type { components } from 'api-types'
|
|
import { HttpResponse } from 'msw'
|
|
import { describe, expect, test, vi } from 'vitest'
|
|
|
|
import { ReplicationPipelineStatus } from './ReplicationPipelineStatus'
|
|
import { PipelineRequestStatusProvider } from '@/state/replication-pipeline-request-status'
|
|
import { customRender } from '@/tests/lib/custom-render'
|
|
import { addAPIMock } from '@/tests/lib/msw'
|
|
|
|
vi.mock('common', async (importOriginal) => ({
|
|
...(await importOriginal<typeof import('common')>()),
|
|
useParams: () => ({ ref: 'default', pipelineId: '42' }),
|
|
}))
|
|
|
|
type PipelineResponse = components['schemas']['PipelineResponse_Output']
|
|
type PipelineStatusResponse = components['schemas']['PipelineStatusResponse_Output']
|
|
type PipelineReplicationStatusResponse =
|
|
components['schemas']['PipelineReplicationStatusResponse_Output']
|
|
|
|
const pipeline: PipelineResponse = {
|
|
id: 42,
|
|
config: { publication_name: 'analytics_publication' },
|
|
destination_id: 7,
|
|
destination_name: 'Analytics warehouse',
|
|
replicator_id: 1,
|
|
source_id: 2,
|
|
source_name: 'main-db',
|
|
tenant_id: 'default',
|
|
}
|
|
|
|
const mockPipelineQueries = ({
|
|
tableStatuses = [],
|
|
}: {
|
|
tableStatuses?: PipelineReplicationStatusResponse['table_statuses']
|
|
} = {}) => {
|
|
addAPIMock({
|
|
method: 'get',
|
|
path: '/platform/replication/:ref/pipelines/:pipeline_id',
|
|
response: () => HttpResponse.json<PipelineResponse>(pipeline),
|
|
})
|
|
addAPIMock({
|
|
method: 'get',
|
|
path: '/platform/replication/:ref/pipelines/:pipeline_id/status',
|
|
response: () =>
|
|
HttpResponse.json<PipelineStatusResponse>({
|
|
pipeline_id: 42,
|
|
status: { name: 'started' },
|
|
}),
|
|
})
|
|
addAPIMock({
|
|
method: 'get',
|
|
path: '/platform/replication/:ref/pipelines/:pipeline_id/replication-status',
|
|
response: () =>
|
|
HttpResponse.json<PipelineReplicationStatusResponse>({
|
|
pipeline_id: 42,
|
|
apply_lag: {
|
|
active: true,
|
|
wal_status: 'reserved',
|
|
restart_lsn_bytes: 0,
|
|
confirmed_flush_lsn_bytes: 0,
|
|
safe_wal_size_bytes: null,
|
|
},
|
|
table_statuses: tableStatuses,
|
|
}),
|
|
})
|
|
}
|
|
|
|
describe('ReplicationPipelineStatus', () => {
|
|
test('preserves the overview structure while pipeline details load', async () => {
|
|
let resolvePipeline: (value: PipelineResponse) => void = () => {}
|
|
let resolvePipelineStatus: (value: PipelineStatusResponse) => void = () => {}
|
|
const pipelineResponse = new Promise<PipelineResponse>((resolve) => {
|
|
resolvePipeline = resolve
|
|
})
|
|
const pipelineStatusResponse = new Promise<PipelineStatusResponse>((resolve) => {
|
|
resolvePipelineStatus = resolve
|
|
})
|
|
|
|
addAPIMock({
|
|
method: 'get',
|
|
path: '/platform/replication/:ref/pipelines/:pipeline_id',
|
|
response: async () => HttpResponse.json<PipelineResponse>(await pipelineResponse),
|
|
})
|
|
addAPIMock({
|
|
method: 'get',
|
|
path: '/platform/replication/:ref/pipelines/:pipeline_id/status',
|
|
response: async () => HttpResponse.json<PipelineStatusResponse>(await pipelineStatusResponse),
|
|
})
|
|
addAPIMock({
|
|
method: 'get',
|
|
path: '/platform/replication/:ref/pipelines/:pipeline_id/replication-status',
|
|
response: () =>
|
|
HttpResponse.json<PipelineReplicationStatusResponse>({
|
|
pipeline_id: 42,
|
|
apply_lag: {
|
|
active: true,
|
|
wal_status: 'reserved',
|
|
restart_lsn_bytes: 0,
|
|
confirmed_flush_lsn_bytes: 0,
|
|
safe_wal_size_bytes: null,
|
|
},
|
|
table_statuses: [],
|
|
}),
|
|
})
|
|
|
|
customRender(
|
|
<PipelineRequestStatusProvider>
|
|
<ReplicationPipelineStatus />
|
|
</PipelineRequestStatusProvider>
|
|
)
|
|
|
|
expect(screen.getByRole('status')).toHaveTextContent('Loading pipeline details')
|
|
expect(screen.getByRole('heading', { name: 'Pipeline health' })).toBeVisible()
|
|
expect(screen.getByRole('heading', { name: 'Replicated tables' })).toBeVisible()
|
|
expect(screen.getByRole('columnheader', { name: 'Table' })).toBeVisible()
|
|
expect(screen.getByRole('columnheader', { name: 'Status' })).toBeVisible()
|
|
expect(screen.getByRole('columnheader', { name: 'Details' })).toBeVisible()
|
|
|
|
resolvePipeline(pipeline)
|
|
|
|
expect(screen.getByRole('status')).toHaveTextContent('Loading pipeline details')
|
|
|
|
resolvePipelineStatus({
|
|
pipeline_id: 42,
|
|
status: { name: 'started' },
|
|
})
|
|
|
|
expect(await screen.findByText('No table data yet')).toBeVisible()
|
|
expect(screen.getByRole('status')).toHaveTextContent('')
|
|
})
|
|
|
|
test('defaults to sorting replicated tables by schema and name', async () => {
|
|
// API order is reverse of schema.name, and statuses would reorder differently if status
|
|
// were the default (error before copying before live).
|
|
mockPipelineQueries({
|
|
tableStatuses: [
|
|
{
|
|
id: 3,
|
|
schema: 'public',
|
|
name: 'zebra',
|
|
table_id: 3,
|
|
table_name: 'public.zebra',
|
|
state: {
|
|
name: 'error',
|
|
reason: 'sync failed',
|
|
retry_policy: { policy: 'manual_retry' },
|
|
},
|
|
},
|
|
{
|
|
id: 2,
|
|
schema: 'public',
|
|
name: 'alpha',
|
|
table_id: 2,
|
|
table_name: 'public.alpha',
|
|
state: { name: 'following_wal' },
|
|
},
|
|
{
|
|
id: 1,
|
|
schema: 'auth',
|
|
name: 'users',
|
|
table_id: 1,
|
|
table_name: 'auth.users',
|
|
state: { name: 'copying_table' },
|
|
},
|
|
],
|
|
})
|
|
|
|
customRender(
|
|
<PipelineRequestStatusProvider>
|
|
<ReplicationPipelineStatus />
|
|
</PipelineRequestStatusProvider>
|
|
)
|
|
|
|
expect(await screen.findByText('zebra')).toBeVisible()
|
|
expect(screen.getByRole('columnheader', { name: 'Table' })).toHaveAttribute(
|
|
'aria-sort',
|
|
'ascending'
|
|
)
|
|
|
|
const table = screen.getByRole('table')
|
|
const tableNames = within(table)
|
|
.getAllByRole('row')
|
|
.map((row) => within(row).queryAllByRole('cell')[0]?.textContent)
|
|
.filter((name): name is string => Boolean(name))
|
|
|
|
expect(tableNames).toEqual(['auth.users', 'public.alpha', 'public.zebra'])
|
|
})
|
|
})
|