From c8371165984b00a1f068dfd511d6251469b9fa00 Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Tue, 22 Sep 2026 16:15:18 +1000 Subject: [PATCH] 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//database/replication/`. 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. ## Summary by CodeRabbit * **Improvements** * Replicated tables are now sorted alphabetically by schema and table name by default. --- .../ReplicationPipelineStatus.test.tsx | 96 ++++++++++++++++++- .../ReplicationPipelineStatus.tsx | 2 +- 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.test.tsx b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.test.tsx index 3006b3ed5fa..91f107ef1d1 100644 --- a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.test.tsx +++ b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.test.tsx @@ -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(pipeline), + }) + addAPIMock({ + method: 'get', + path: '/platform/replication/:ref/pipelines/:pipeline_id/status', + response: () => + HttpResponse.json({ + pipeline_id: 42, + status: { name: 'started' }, + }), + }) + addAPIMock({ + method: 'get', + path: '/platform/replication/:ref/pipelines/:pipeline_id/replication-status', + response: () => + HttpResponse.json({ + 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( + + + + ) + + 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']) + }) }) diff --git a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.tsx b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.tsx index ef91a267b74..a845ea53c59 100644 --- a/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.tsx +++ b/apps/studio/components/interfaces/Database/Replication/ReplicationPipelineStatus/ReplicationPipelineStatus.tsx @@ -166,7 +166,7 @@ export const ReplicationPipelineStatus = () => { const applyLagMetrics = replicationStatusData?.apply_lag - const [sort, setSort] = useState('status:asc') + const [sort, setSort] = useState('table:asc') const [sortColumn, sortDirection] = sort.split(':') as [TableSortColumn, 'asc' | 'desc'] const getAriaSort = (column: TableSortColumn) => {