Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3190469
fix: RTA details pane arrows follow filtered table rows
mattiasimonato May 21, 2026
808f58e
test: add RTA overview filtered navigation tests
mattiasimonato May 21, 2026
d22e2e7
Merge branch 'v3' into PMM-14901-rta-details-arrows-respect-filters
mattiasimonato May 21, 2026
c4b5954
fix: lint warnings
mattiasimonato May 21, 2026
64567bb
fix: sync RTA details navigation with table sort order
mattiasimonato May 22, 2026
f914530
fix: disable RTA details navigation when selection is not navigable
mattiasimonato May 22, 2026
5aa136a
fix: keep RTA overview column-only filtering
mattiasimonato May 22, 2026
4ed774d
test: wait for RTA navigation assertions after pane updates
mattiasimonato May 22, 2026
3de3b97
Merge branch 'v3' into PMM-14901-rta-details-arrows-respect-filters
mattiasimonato May 22, 2026
f84b62c
fix: prevent RTA overview navigable-sync render loop
mattiasimonato May 22, 2026
2fcd4ad
fix: remove unused imports in OverviewTable utils
mattiasimonato May 22, 2026
5f5035b
fix: type resolveTableStateUpdate for tsc build
mattiasimonato May 22, 2026
8549392
refactor: use plain MRT setters for RTA overview table
mattiasimonato May 22, 2026
84560c0
Merge branch 'v3' into PMM-14901-rta-details-arrows-respect-filters
mattiasimonato May 22, 2026
a280c7a
fix: always sync RTA navigable queries on table updates
mattiasimonato May 22, 2026
dc823c7
Merge branch 'v3' into PMM-14901-rta-details-arrows-respect-filters
mattiasimonato May 22, 2026
97de9b0
Merge branch 'v3' into PMM-14901-rta-details-arrows-respect-filters
mattiasimonato May 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* Details-pane prev/next must follow the table's filtered rows (navigableQueries),
* not the full list returned by the API.
*
* In the app, OverviewTable syncs navigableQueries from Material React Table after
* filters are applied. RealtimeOverview uses that list for arrow navigation.
*
* We mock OverviewTable here so we can fix navigableQueries (query-1, query-2) while
* the API still returns an extra query (query-3) that would be next if we used the
* unfiltered array. RealtimeOverview.test.tsx keeps the real table for other behavior.
*/
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { FC, useEffect } from 'react';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { wrapWithQueryProvider } from 'utils/testUtils';
import { TEST_MONGO_DB_QUERY_DATA, TEST_REAL_TIME_SESSION } from 'utils/testStubs';
import { QueryData } from 'types/rta.types';
import RealtimeOverview from './RealtimeOverview';

const queryOne: QueryData = {
...TEST_MONGO_DB_QUERY_DATA,
queryId: 'query-1',
};

const queryTwo: QueryData = {
...TEST_MONGO_DB_QUERY_DATA,
queryId: 'query-2',
};

const queryFilteredOut: QueryData = {
...TEST_MONGO_DB_QUERY_DATA,
queryId: 'query-3',
};

const navigableQueries = [queryOne, queryTwo];

const { searchQueries, getRunningSessions } = vi.hoisted(() => ({
searchQueries: vi.fn(),
getRunningSessions: vi.fn(),
}));

vi.mock('api/rta', () => ({
searchQueries,
getRunningSessions,
}));

vi.mock('./table/OverviewTable', () => {
const MockOverviewTable: FC<{
onQuerySelected: (query: QueryData) => void;
onNavigableQueriesChange: (queries: QueryData[]) => void;
}> = ({ onQuerySelected, onNavigableQueriesChange }) => {
useEffect(() => {
onNavigableQueriesChange(navigableQueries);
}, [onNavigableQueriesChange]);

return (
<button
type="button"
data-testid="mock-select-first-query"
onClick={() => onQuerySelected(queryOne)}
>
Select first query
</button>
);
};

return { default: MockOverviewTable };
});

const renderComponent = () =>
render(
wrapWithQueryProvider(
<MemoryRouter
initialEntries={[
`/rta/overview?serviceIds=${TEST_REAL_TIME_SESSION.serviceId}`,
]}
>
<Routes>
<Route path="/rta/overview" element={<RealtimeOverview />} />
</Routes>
</MemoryRouter>
)
);

const openDetailsPaneOnFirstQuery = async () => {
await waitFor(() =>
expect(screen.getByTestId('mock-select-first-query')).toBeInTheDocument()
);
fireEvent.click(screen.getByTestId('mock-select-first-query'));
await waitFor(() =>
expect(screen.getByTestId('query-details-pane')).toHaveAttribute(
'aria-hidden',
'false'
)
);
};

const getOperationId = () => screen.getByTestId('operation-id-value');

describe('RealtimeOverview details pane navigation', () => {
beforeEach(() => {
vi.clearAllMocks();

searchQueries.mockResolvedValue({
queries: [...navigableQueries, queryFilteredOut],
});

getRunningSessions.mockResolvedValue([TEST_REAL_TIME_SESSION]);
});

it('navigates to the next visible query, not the next API query', async () => {
renderComponent();
await openDetailsPaneOnFirstQuery();

expect(getOperationId()).toHaveTextContent('query-1');

fireEvent.click(screen.getByTestId('details-pane-next-button'));

expect(getOperationId()).toHaveTextContent('query-2');
expect(getOperationId()).not.toHaveTextContent('query-3');
Comment thread
mattiasimonato marked this conversation as resolved.
});

it('navigates to the previous visible query', async () => {
renderComponent();
await openDetailsPaneOnFirstQuery();

fireEvent.click(screen.getByTestId('details-pane-next-button'));
expect(getOperationId()).toHaveTextContent('query-2');

fireEvent.click(screen.getByTestId('details-pane-prev-button'));
expect(getOperationId()).toHaveTextContent('query-1');
});

it('disables prev on the first visible query and next on the last', async () => {
renderComponent();
await openDetailsPaneOnFirstQuery();

expect(screen.getByTestId('details-pane-prev-button')).toBeDisabled();
expect(screen.getByTestId('details-pane-next-button')).not.toBeDisabled();

fireEvent.click(screen.getByTestId('details-pane-next-button'));

expect(screen.getByTestId('details-pane-prev-button')).not.toBeDisabled();
expect(screen.getByTestId('details-pane-next-button')).toBeDisabled();
});
});
38 changes: 18 additions & 20 deletions ui/apps/pmm/src/pages/rta/overview/RealtimeOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,39 +29,36 @@ const RealtimeOverviewPage: FC = () => {
refetchInterval: refreshInterval,
}
);
const [selectedQueryIndex, setSelectedQueryIndex] = useState<number>();
// Synced from the table after filters; details-pane arrows use this list, not the full API result.
const [navigableQueries, setNavigableQueries] = useState<QueryData[]>([]);
const [selectedQuery, setSelectedQuery] = useState<QueryData>();
// We need to store the previous fetching state to restore it when the details pane is closed
const previousFetchingState = useRef<boolean>(fetching);
const { data: sessions = [], isLoading } = useRealtimeSessions();

const handleQueryChange = (query: QueryData, index: number) => {
const selectedQueryIndex = selectedQuery
? navigableQueries.findIndex(
(query) => query.queryId === selectedQuery.queryId
)
: -1;

Comment thread
mattiasimonato marked this conversation as resolved.
const handleQuerySelected = (query: QueryData) => {
setSelectedQuery(query);
setSelectedQueryIndex(index);
previousFetchingState.current = fetching;
setFetching(false);
};

const handleCloseDetails = () => {
setSelectedQuery(undefined);
setSelectedQueryIndex(undefined);
setFetching(previousFetchingState.current);
};

const handleNextQuery = () => {
const idx = (selectedQueryIndex || 0) + 1;
if (idx >= queries.length) {
return;
}
handleQueryChange(queries[idx], idx);
};

const handlePreviousQuery = () => {
const idx = (selectedQueryIndex || 0) - 1;
if (idx < 0) {
const handleAdjacentQuery = (offset: -1 | 1) => {
const nextIndex = selectedQueryIndex + offset;
if (nextIndex < 0 || nextIndex >= navigableQueries.length) {
return;
}
handleQueryChange(queries[idx], idx);
handleQuerySelected(navigableQueries[nextIndex]);
};
Comment thread
mattiasimonato marked this conversation as resolved.

const handleServiceIdsChange = (newServiceIds: string[]) => {
Expand Down Expand Up @@ -94,7 +91,8 @@ const RealtimeOverviewPage: FC = () => {
<RealtimePage>
<OverviewTable
queries={queries || []}
onQuerySelected={handleQueryChange}
onQuerySelected={handleQuerySelected}
onNavigableQueriesChange={setNavigableQueries}
actions={() => (
<Stack
direction="row"
Expand Down Expand Up @@ -177,9 +175,9 @@ const RealtimeOverviewPage: FC = () => {
query={selectedQuery}
onClose={handleCloseDetails}
isFirstQuery={selectedQueryIndex === 0}
isLastQuery={selectedQueryIndex === queries.length - 1}
onNext={handleNextQuery}
onPrevious={handlePreviousQuery}
isLastQuery={selectedQueryIndex === navigableQueries.length - 1}
onNext={() => handleAdjacentQuery(1)}
onPrevious={() => handleAdjacentQuery(-1)}
/>
</RealtimePage>
);
Expand Down
118 changes: 76 additions & 42 deletions ui/apps/pmm/src/pages/rta/overview/table/OverviewTable.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { type MRT_Row } from 'material-react-table';
import { MaterialReactTableProps } from 'material-react-table';
import {
type MRT_ColumnFiltersState,
type MRT_Row,
type MRT_TableInstance,
MaterialReactTableProps,
} from 'material-react-table';
import { Table } from '@percona/percona-ui';
import { FC } from 'react';
import { FC, useCallback, useEffect, useRef, useState } from 'react';
import { QueryData } from 'types/rta.types';
import { OVERVIEW_TABLE_COLUMNS } from './OverviewTable.constants';
import { RealtimeTableWrapper } from 'pages/rta/components/rta-table-wrapper';
Expand All @@ -11,54 +15,84 @@ import { filterElapsedTime } from './OverviewTable.utils';

interface Props {
queries: QueryData[];
onQuerySelected: (query: QueryData, idx: number) => void;
onQuerySelected: (query: QueryData) => void;
onNavigableQueriesChange: (queries: QueryData[]) => void;
actions?: MaterialReactTableProps<QueryData>['renderTopToolbarCustomActions'];
onRowHover?: () => void;
}

const OverviewTable: FC<Props> = ({
queries,
onQuerySelected,
onNavigableQueriesChange,
actions,
onRowHover,
}) => (
<RealtimeTableWrapper>
<Table
tableName="realtime-overview-table"
initialState={{
pagination: {
pageSize: 25,
pageIndex: 0,
},
}}
columns={OVERVIEW_TABLE_COLUMNS}
data={queries}
noDataMessage={Messages.noData}
muiTopToolbarProps={{
sx: {
// vertically center the buttons
[`& > .${boxClasses.root}`]: {
alignItems: 'center',
flexDirection: 'row-reverse',
}) => {
const tableRef = useRef<MRT_TableInstance<QueryData> | null>(null);
// Controlled filter state is required to read the filtered row model via tableInstanceRef.
const [columnFilters, setColumnFilters] = useState<MRT_ColumnFiltersState>([]);
const [globalFilter, setGlobalFilter] = useState('');

// Pre-pagination so navigation covers all filtered rows, not only the current page.
const getNavigableQueries = useCallback(
() =>
tableRef.current?.getPrePaginationRowModel().rows.map((row) => row.original) ??
queries,
[queries]
);

useEffect(() => {
onNavigableQueriesChange(getNavigableQueries());
}, [
columnFilters,
getNavigableQueries,
globalFilter,
onNavigableQueriesChange,
]);

return (
<RealtimeTableWrapper>
<Table
tableName="realtime-overview-table"
initialState={{
pagination: {
pageSize: 25,
pageIndex: 0,
},
}}
columns={OVERVIEW_TABLE_COLUMNS}
data={queries}
noDataMessage={Messages.noData}
muiTopToolbarProps={{
sx: {
// vertically center the buttons
[`& > .${boxClasses.root}`]: {
alignItems: 'center',
flexDirection: 'row-reverse',
},
},
},
}}
enableGlobalFilter={false}
enableHiding={false}
enableRowHoverAction
rowHoverAction={(row) => onQuerySelected(row.original, row.index)}
renderTopToolbarCustomActions={actions}
filterFns={{
// default 'betweenInclusive' filter fails on values like '1.50', discarding the row that has 1.5 seconds
timeRangeFilterFn: (row, id, filterValue) =>
filterElapsedTime(row as MRT_Row<QueryData>, id, filterValue),
}}
muiTableBodyRowProps={({ row }) => ({
onMouseEnter: onRowHover,
'data-testid': `query-${row.original.queryId}-row`,
})}
/>
</RealtimeTableWrapper>
);
}}
state={{ columnFilters, globalFilter }}
onColumnFiltersChange={setColumnFilters}
onGlobalFilterChange={setGlobalFilter}
enableGlobalFilter={true}
Comment thread
fabio-silva marked this conversation as resolved.
Outdated
enableHiding={false}
enableRowHoverAction
tableInstanceRef={tableRef}
rowHoverAction={(row) => onQuerySelected(row.original)}
renderTopToolbarCustomActions={actions}
filterFns={{
// default 'betweenInclusive' filter fails on values like '1.50', discarding the row that has 1.5 seconds
timeRangeFilterFn: (row, id, filterValue) =>
filterElapsedTime(row as MRT_Row<QueryData>, id, filterValue),
}}
muiTableBodyRowProps={({ row }) => ({
onMouseEnter: onRowHover,
'data-testid': `query-${row.original.queryId}-row`,
})}
/>
</RealtimeTableWrapper>
);
};

export default OverviewTable;
Loading