Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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;
// 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