-
Notifications
You must be signed in to change notification settings - Fork 216
PMM-14901: RTA details arrows respect filters #5397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v3
Are you sure you want to change the base?
Changes from 16 commits
3190469
808f58e
d22e2e7
c4b5954
64567bb
f914530
5aa136a
4ed774d
3de3b97
f84b62c
2fcd4ad
5f5035b
8549392
84560c0
a280c7a
dc823c7
97de9b0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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'); | ||
| }); | ||
|
|
||
| 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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Or is there other reason to declare
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||||||
|
|
||||||
|
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]); | ||||||
| }; | ||||||
|
mattiasimonato marked this conversation as resolved.
|
||||||
|
|
||||||
| const handleServiceIdsChange = (newServiceIds: string[]) => { | ||||||
|
|
@@ -93,8 +96,9 @@ const RealtimeOverviewPage: FC = () => { | |||||
| return ( | ||||||
| <RealtimePage> | ||||||
| <OverviewTable | ||||||
| queries={queries || []} | ||||||
| onQuerySelected={handleQueryChange} | ||||||
| queries={tableQueries} | ||||||
| onQuerySelected={handleQuerySelected} | ||||||
| onNavigableQueriesChange={setNavigableQueries} | ||||||
| actions={() => ( | ||||||
| <Stack | ||||||
| direction="row" | ||||||
|
|
@@ -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> | ||||||
| ); | ||||||
|
|
||||||
Uh oh!
There was an error while loading. Please reload this page.