fix(studio): sort replicated tables by name by default (#50696)

## 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 -->
This commit is contained in:
Danny White authored and GitHub committed 2026-09-22 16:15:18 +10:00
1 parent 47592ace0f
commit c837116598
2 files changed
+96 -2

No files matched your search

@@ -1,4 +1,4 @@
import { screen } from '@testing-library/react'
import { screen, within } from '@testing-library/react'
import type { components } from 'api-types'
import { HttpResponse } from 'msw'
import { describe, expect, test, vi } from 'vitest'
@@ -29,6 +29,43 @@ const pipeline: PipelineResponse = {
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 = () => {}
@@ -92,4 +129,61 @@ describe('ReplicationPipelineStatus', () => {
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'])
})
})
@@ -166,7 +166,7 @@ export const ReplicationPipelineStatus = () => {
const applyLagMetrics = replicationStatusData?.apply_lag
const [sort, setSort] = useState<TableSort>('status:asc')
const [sort, setSort] = useState<TableSort>('table:asc')
const [sortColumn, sortDirection] = sort.split(':') as [TableSortColumn, 'asc' | 'desc']
const getAriaSort = (column: TableSortColumn) => {