Skip to content
Open
Show file tree
Hide file tree
Changes from 16 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,197 @@
/**
* 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, mockNavigableQueries } = vi.hoisted(() => {
let navigable: QueryData[] = [];

return {
searchQueries: vi.fn(),
getRunningSessions: vi.fn(),
mockNavigableQueries: {
get: () => navigable,
set: (queries: QueryData[]) => {
navigable = queries;
},
},
};
});

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(mockNavigableQueries.get());
}, [onNavigableQueriesChange]);

return (
<>
<button
type="button"
data-testid="mock-select-first-query"
onClick={() => onQuerySelected(queryOne)}
>
Select first query
</button>
<button
type="button"
data-testid="mock-drop-selected-from-navigable"
onClick={() => {
mockNavigableQueries.set([queryTwo]);
onNavigableQueriesChange(mockNavigableQueries.get());
}}
>
Drop selected from navigable
</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();
mockNavigableQueries.set(navigableQueries);

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'));

await waitFor(() => {
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'));
await waitFor(() => {
expect(getOperationId()).toHaveTextContent('query-2');
});

fireEvent.click(screen.getByTestId('details-pane-prev-button'));
await waitFor(() => {
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'));

await waitFor(() => {
expect(screen.getByTestId('details-pane-prev-button')).not.toBeDisabled();
expect(screen.getByTestId('details-pane-next-button')).toBeDisabled();
});
});

it('disables navigation when the selected query is not in navigableQueries', async () => {
renderComponent();
await openDetailsPaneOnFirstQuery();

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

fireEvent.click(screen.getByTestId('mock-drop-selected-from-navigable'));

await waitFor(() => {
expect(screen.getByTestId('details-pane-prev-button')).toBeDisabled();
expect(screen.getByTestId('details-pane-next-button')).toBeDisabled();
});

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

await waitFor(() => {
expect(getOperationId()).toHaveTextContent('query-1');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ describe('RealtimeOverview', () => {

expect(screen.getByTestId('auto-refresh-button')).not.toBeDisabled();

const clearButton = await waitFor(() => screen.findByTitle('Clear'));
const clearButton = await screen.findByTitle('Clear');
fireEvent.click(clearButton);

expect(screen.getByTestId('auto-refresh-button')).toBeDisabled();
Expand Down Expand Up @@ -234,7 +234,7 @@ describe('RealtimeOverview', () => {

expect(screen.getByTestId('auto-refresh-button')).toBeDisabled();

const openButton = await waitFor(() => screen.findByTitle('Open'));
const openButton = await screen.findByTitle('Open');
fireEvent.click(openButton);

const serviceOptionId =
Expand Down Expand Up @@ -265,7 +265,7 @@ describe('RealtimeOverview', () => {

expect(screen.getByTestId('auto-refresh-button')).toBeDisabled();

const openButton = await waitFor(() => screen.findByTitle('Open'));
const openButton = await screen.findByTitle('Open');
fireEvent.click(openButton);

const serviceOptionId =
Expand Down
49 changes: 28 additions & 21 deletions ui/apps/pmm/src/pages/rta/overview/RealtimeOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,51 +17,54 @@ import Button from '@mui/material/Button';
import { ServicesAutocompleteInput } from '../components/services-autocomplete-input';
import { AutoRefreshSelect } from './auto-refresh-select';

const EMPTY_QUERIES: QueryData[] = [];

const RealtimeOverviewPage: FC = () => {
const [searchParams, setSearchParams] = useSearchParams();
const serviceIds = searchParams.getAll('serviceIds');
const [fetching, setFetching] = useState(serviceIds.length > 0);
const [refreshInterval, setRefreshInterval] = useState(2000);
const { data: queries = [], refetch } = useRealtimeQueries(
const { data: queries, refetch } = useRealtimeQueries(
{ serviceIds },
{
enabled: fetching,
refetchInterval: refreshInterval,
}
);
const [selectedQueryIndex, setSelectedQueryIndex] = useState<number>();
const tableQueries = queries ?? EMPTY_QUERIES;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const tableQueries = queries ?? EMPTY_QUERIES;
const tableQueries: QueryData[] = queries ?? [];

Or is there other reason to declare EMPTY_QUERIES?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likely, yes. So now it's in RTA and planned for Alerts. I see it potentially coming to Inventory as well, so this panel may be restricted to always consuming lists from MRT at Peak Design level

// 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) {
const handleAdjacentQuery = (offset: -1 | 1) => {
if (selectedQueryIndex < 0) {
return;
}
handleQueryChange(queries[idx], idx);
};

const handlePreviousQuery = () => {
const idx = (selectedQueryIndex || 0) - 1;
if (idx < 0) {
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 @@ -93,8 +96,9 @@ const RealtimeOverviewPage: FC = () => {
return (
<RealtimePage>
<OverviewTable
queries={queries || []}
onQuerySelected={handleQueryChange}
queries={tableQueries}
onQuerySelected={handleQuerySelected}
onNavigableQueriesChange={setNavigableQueries}
actions={() => (
<Stack
direction="row"
Expand Down Expand Up @@ -176,10 +180,13 @@ const RealtimeOverviewPage: FC = () => {
<DetailsPane
query={selectedQuery}
onClose={handleCloseDetails}
isFirstQuery={selectedQueryIndex === 0}
isLastQuery={selectedQueryIndex === queries.length - 1}
onNext={handleNextQuery}
onPrevious={handlePreviousQuery}
isFirstQuery={selectedQueryIndex <= 0}
isLastQuery={
selectedQueryIndex < 0 ||
selectedQueryIndex >= navigableQueries.length - 1
}
onNext={() => handleAdjacentQuery(1)}
onPrevious={() => handleAdjacentQuery(-1)}
/>
</RealtimePage>
);
Expand Down
Loading
Loading