Skip to content

Commit

Permalink
feat: add unified data grid page change callback
Browse files Browse the repository at this point in the history
  • Loading branch information
logeekal committed Nov 21, 2024
1 parent f473762 commit e75c2c2
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 14 deletions.
13 changes: 12 additions & 1 deletion packages/kbn-unified-data-table/src/components/data_table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,12 @@ export interface UnifiedDataTableProps {
* @param row
*/
getRowIndicator?: ColorIndicatorControlColumnParams['getRowIndicator'];
/**
*
* this callback is triggered when user navigates to a different page
*
*/
onChangePage?: (pageIndex: number) => void;
}

export const EuiDataGridMemoized = React.memo(EuiDataGrid);
Expand Down Expand Up @@ -493,6 +499,7 @@ export const UnifiedDataTable = ({
getRowIndicator,
dataGridDensityState,
onUpdateDataGridDensity,
onChangePage: onChangePageProp,
}: UnifiedDataTableProps) => {
const { fieldFormats, toastNotifications, dataViewFieldEditor, uiSettings, storage, data } =
services;
Expand Down Expand Up @@ -611,9 +618,12 @@ export const UnifiedDataTable = ({
onUpdateRowsPerPage?.(pageSize);
};

const onChangePage = (pageIndex: number) =>
const onChangePage = (pageIndex: number) => {
setPagination((paginationData) => ({ ...paginationData, pageIndex }));

onChangePageProp?.(pageIndex);
};

return isPaginationEnabled
? {
onChangeItemsPerPage,
Expand All @@ -630,6 +640,7 @@ export const UnifiedDataTable = ({
pageCount,
rowsPerPageOptions,
onUpdateRowsPerPage,
onChangePageProp,
]);

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import { createExpandableFlyoutApiMock } from '../../../../../common/mock/expandable_flyout';
import { OPEN_FLYOUT_BUTTON_TEST_ID } from '../../../../../notes/components/test_ids';
import { userEvent } from '@testing-library/user-event';
import * as notesApi from '../../../../../notes/api/api';

jest.mock('../../../../../common/components/user_privileges');

Expand Down Expand Up @@ -154,7 +155,9 @@ const { storage: storageMock } = createSecuritySolutionStorageMock();
let useTimelineEventsMock = jest.fn();

describe('query tab with unified timeline', () => {
const fetchNotesMock = jest.spyOn(notesApi, 'fetchNotesByDocumentIds');
beforeAll(() => {
fetchNotesMock.mockImplementation(jest.fn());
jest.mocked(useExpandableFlyoutApi).mockImplementation(() => ({
...createExpandableFlyoutApiMock(),
openFlyout: mockOpenFlyout,
Expand All @@ -176,6 +179,7 @@ describe('query tab with unified timeline', () => {
afterEach(() => {
jest.clearAllMocks();
storageMock.clear();
fetchNotesMock.mockClear();
cleanup();
localStorage.clear();
});
Expand Down Expand Up @@ -424,6 +428,130 @@ describe('query tab with unified timeline', () => {
},
SPECIAL_TEST_TIMEOUT
);

it(
'should load notes for current page only',
async () => {
const mockStateWithNoteInTimeline = {
...mockGlobalState,
timeline: {
...mockGlobalState.timeline,
timelineById: {
[TimelineId.test]: {
...mockGlobalState.timeline.timelineById[TimelineId.test],
/* 1 record for each page */
itemsPerPage: 1,
pageIndex: 0,
itemsPerPageOptions: [1, 2, 3, 4, 5],
savedObjectId: 'timeline-1', // match timelineId in mocked notes data
pinnedEventIds: { '1': true },
},
},
},
};

const { container } = render(
<TestProviders
store={createMockStore({
...structuredClone(mockStateWithNoteInTimeline),
})}
>
<TestComponent />
</TestProviders>
);

expect(await screen.findByTestId('discoverDocTable')).toBeVisible();

expect(screen.getByTestId('pagination-button-previous')).toBeVisible();

expect(screen.getByTestId('pagination-button-0')).toHaveAttribute('aria-current', 'true');
expect(fetchNotesMock).toHaveBeenCalledWith(['1']);

// Page : 2

fetchNotesMock.mockClear();
expect(screen.getByTestId('pagination-button-1')).toBeVisible();

fireEvent.click(screen.getByTestId('pagination-button-1'));

await waitFor(() => {
expect(screen.getByTestId('pagination-button-1')).toHaveAttribute('aria-current', 'true');

expect(fetchNotesMock).toHaveBeenNthCalledWith(1, [mockTimelineData[1]._id]);
});

// Page : 3

fetchNotesMock.mockClear();
expect(screen.getByTestId('pagination-button-2')).toBeVisible();
fireEvent.click(screen.getByTestId('pagination-button-2'));

await waitFor(() => {
expect(screen.getByTestId('pagination-button-2')).toHaveAttribute('aria-current', 'true');

expect(fetchNotesMock).toHaveBeenNthCalledWith(1, [mockTimelineData[2]._id]);
});
},
SPECIAL_TEST_TIMEOUT
);

it(
'should load notes for correct page size',
async () => {
const mockStateWithNoteInTimeline = {
...mockGlobalState,
timeline: {
...mockGlobalState.timeline,
timelineById: {
[TimelineId.test]: {
...mockGlobalState.timeline.timelineById[TimelineId.test],
/* 1 record for each page */
itemsPerPage: 1,
pageIndex: 0,
itemsPerPageOptions: [1, 2, 3, 4, 5],
savedObjectId: 'timeline-1', // match timelineId in mocked notes data
pinnedEventIds: { '1': true },
},
},
},
};

const { container } = render(
<TestProviders
store={createMockStore({
...structuredClone(mockStateWithNoteInTimeline),
})}
>
<TestComponent />
</TestProviders>
);

expect(await screen.findByTestId('discoverDocTable')).toBeVisible();

expect(screen.getByTestId('pagination-button-previous')).toBeVisible();

expect(screen.getByTestId('pagination-button-0')).toHaveAttribute('aria-current', 'true');
expect(screen.getByTestId('tablePaginationPopoverButton')).toHaveTextContent(
'Rows per page: 1'
);
fireEvent.click(screen.getByTestId('tablePaginationPopoverButton'));

await waitFor(() => {
expect(screen.getByTestId('tablePagination-2-rows')).toBeVisible();
});

fetchNotesMock.mockClear();
fireEvent.click(screen.getByTestId('tablePagination-2-rows'));

await waitFor(() => {
expect(fetchNotesMock).toHaveBeenNthCalledWith(1, [
mockTimelineData[0]._id,
mockTimelineData[1]._id,
]);
});
},
SPECIAL_TEST_TIMEOUT
);
});

describe('columns', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { getEsQueryConfig } from '@kbn/data-plugin/common';
import { DataLoadingState } from '@kbn/unified-data-table';
import { useExpandableFlyoutApi } from '@kbn/expandable-flyout';
import type { RunTimeMappings } from '@kbn/timelines-plugin/common/search_strategy';
import { useFetchNotes } from '../../../../../notes/hooks/use_fetch_notes';
import {
DocumentDetailsLeftPanelKey,
DocumentDetailsRightPanelKey,
Expand Down Expand Up @@ -104,7 +105,7 @@ export const QueryTabContentComponent: React.FC<Props> = ({
getManageTimeline(state, timelineId ?? TimelineId.active)
);

const { sampleSize } = currentTimeline;
const { sampleSize, pageIndex = 0 } = currentTimeline;

const esQueryConfig = useMemo(() => getEsQueryConfig(uiSettings), [uiSettings]);
const kqlQuery: {
Expand Down Expand Up @@ -182,8 +183,21 @@ export const QueryTabContentComponent: React.FC<Props> = ({
sort: timelineQuerySortField,
startDate: start,
timerangeKind,
pageIndex,
pageSize: itemsPerPage,
});

const { onLoad } = useFetchNotes();

useEffect(() => {
const eventsOnCurrentPage = events.slice(
itemsPerPage * pageIndex,
itemsPerPage * (pageIndex + 1)
);

onLoad(eventsOnCurrentPage);
}, [events, pageIndex, itemsPerPage, onLoad]);

const { openFlyout } = useExpandableFlyoutApi();
const securitySolutionNotesDisabled = useIsExperimentalFeatureEnabled(
'securitySolutionNotesDisabled'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,13 @@ export const TimelineDataTableComponent: React.FC<DataTableProps> = memo(
return enabledRowRenderers.length > 0 ? trailingControlColumns : undefined;
}, [enabledRowRenderers.length, trailingControlColumns]);

const onDataGridPageChange = useCallback(
(newPageIndex: number) => {
dispatch(timelineActions.setPageIndex({ id: timelineId, pageIndex: newPageIndex }));
},
[dispatch, timelineId]
);

return (
<StatefulEventContext.Provider value={activeStatefulEventContext}>
<StyledTimelineUnifiedDataTable>
Expand Down Expand Up @@ -424,6 +431,7 @@ export const TimelineDataTableComponent: React.FC<DataTableProps> = memo(
renderCustomGridBody={finalRenderCustomBodyCallback}
trailingControlColumns={finalTrailControlColumns}
externalControlColumns={leadingControlColumns}
onChangePage={onDataGridPageChange}
/>
</StyledTimelineUnifiedDataTable>
</StatefulEventContext.Provider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ import type {
} from '../../../common/search_strategy/timeline/events/eql';
import { useTrackHttpRequest } from '../../common/lib/apm/use_track_http_request';
import { APP_UI_ID } from '../../../common/constants';
import { useFetchNotes } from '../../notes/hooks/use_fetch_notes';

export interface TimelineArgs {
events: TimelineItem[];
Expand Down Expand Up @@ -97,6 +96,8 @@ export interface UseTimelineEventsProps {
startDate?: string;
timerangeKind?: 'absolute' | 'relative';
fetchNotes?: boolean;
pageIndex?: number;
pageSize?: number;
}

const getTimelineEvents = (timelineEdges: TimelineEdges[]): TimelineItem[] =>
Expand Down Expand Up @@ -483,7 +484,6 @@ export const useTimelineEvents = ({
sort = initSortDefault,
skip = false,
timerangeKind,
fetchNotes = true,
}: UseTimelineEventsProps): [DataLoadingState, TimelineArgs] => {
const [dataLoadingState, timelineResponse, timelineSearchHandler] = useTimelineEventsHandler({
dataViewId,
Expand All @@ -501,19 +501,11 @@ export const useTimelineEvents = ({
skip,
timerangeKind,
});
const { onLoad } = useFetchNotes();

const onTimelineSearchComplete: OnNextResponseHandler = useCallback(
(response) => {
if (fetchNotes) onLoad(response.events);
},
[fetchNotes, onLoad]
);

useEffect(() => {
if (!timelineSearchHandler) return;
timelineSearchHandler(onTimelineSearchComplete);
}, [timelineSearchHandler, onTimelineSearchComplete]);
timelineSearchHandler();
}, [timelineSearchHandler]);

return [dataLoadingState, timelineResponse];
};
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,5 @@ export const setConfirmingNoteId = actionCreator<{
id: string;
confirmingNoteId: string | null | undefined;
}>('SET_CONFIRMING_NOTE_ID');

export const setPageIndex = actionCreator<{ id: string; pageIndex: number }>('SET_PAGE_INDEX');
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ export interface TimelineModel {
sampleSize: number;
/** the note id pending deletion */
confirmingNoteId?: string | null;
/** the page index of the table */
pageIndex?: number;
}

export type SubsetTimelineModel = Readonly<
Expand Down Expand Up @@ -197,6 +199,7 @@ export type SubsetTimelineModel = Readonly<
| 'savedSearch'
| 'isDataProviderVisible'
| 'changed'
| 'pageIndex'
>
>;

Expand Down
11 changes: 11 additions & 0 deletions x-pack/plugins/security_solution/public/timelines/store/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
updateColumnWidth,
setConfirmingNoteId,
deleteNoteFromEvent,
setPageIndex,
} from './actions';

import {
Expand Down Expand Up @@ -603,4 +604,14 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState)
},
},
}))
.case(setPageIndex, (state, { id, pageIndex }) => ({
...state,
timelineById: {
...state.timelineById,
[id]: {
...state.timelineById[id],
pageIndex,
},
},
}))
.build();

0 comments on commit e75c2c2

Please sign in to comment.