Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
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();