diff --git a/plugins/.empty b/plugins/.empty
deleted file mode 100644
index e69de29bb2d1..000000000000
diff --git a/x-pack/legacy/plugins/siem/common/constants.ts b/x-pack/legacy/plugins/siem/common/constants.ts
index 1b3628306052..3f54077c2aca 100644
--- a/x-pack/legacy/plugins/siem/common/constants.ts
+++ b/x-pack/legacy/plugins/siem/common/constants.ts
@@ -8,3 +8,4 @@ export const APP_ID = 'siem';
export const APP_NAME = 'SIEM';
export const DEFAULT_INDEX_KEY = 'siem:defaultIndex';
export const DEFAULT_ANOMALY_SCORE = 'siem:defaultAnomalyScore';
+export const DEFAULT_MAX_TABLE_QUERY_SIZE = 10000;
diff --git a/x-pack/legacy/plugins/siem/common/graphql/shared/schema.gql.ts b/x-pack/legacy/plugins/siem/common/graphql/shared/schema.gql.ts
index bf480b2878c9..a3c597c3772f 100644
--- a/x-pack/legacy/plugins/siem/common/graphql/shared/schema.gql.ts
+++ b/x-pack/legacy/plugins/siem/common/graphql/shared/schema.gql.ts
@@ -30,6 +30,17 @@ export const sharedSchema = gql`
tiebreaker: String
}
+ input PaginationInputPaginated {
+ "The activePage parameter defines the page of results you want to fetch"
+ activePage: Float!
+ "The cursorStart parameter defines the start of the results to be displayed"
+ cursorStart: Float!
+ "The fakePossibleCount parameter determines the total count in order to show 5 additional pages"
+ fakePossibleCount: Float!
+ "The querySize parameter is the number of items to be returned"
+ querySize: Float!
+ }
+
enum Direction {
asc
desc
@@ -61,4 +72,10 @@ export const sharedSchema = gql`
dsl: [String!]!
response: [String!]!
}
+
+ type PageInfoPaginated {
+ activePage: Float!
+ fakeTotalCount: Float!
+ showMorePagesIndicator: Boolean!
+ }
`;
diff --git a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx b/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx
index 5fd760bb613a..45fddc5618f7 100644
--- a/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/filters_global/filters_global.tsx
@@ -9,7 +9,7 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import React from 'react';
import { Sticky } from 'react-sticky';
import { pure } from 'recompose';
-import styled from 'styled-components';
+import styled, { css } from 'styled-components';
import { SuperDatePicker } from '../super_date_picker';
@@ -20,7 +20,7 @@ const disableSticky = 'screen and (max-width: ' + euiLightVars.euiBreakpoints.s
const disableStickyMq = window.matchMedia(disableSticky);
const Aside = styled.aside<{ isSticky?: boolean }>`
- ${props => `
+ ${props => css`
position: relative;
z-index: ${props.theme.eui.euiZNavigation};
background: ${props.theme.eui.euiColorEmptyShade};
diff --git a/x-pack/legacy/plugins/siem/public/components/header_panel/header_panel.tsx b/x-pack/legacy/plugins/siem/public/components/header_panel/header_panel.tsx
index 274f4aa53878..c5c7aaf5d244 100644
--- a/x-pack/legacy/plugins/siem/public/components/header_panel/header_panel.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/header_panel/header_panel.tsx
@@ -7,12 +7,12 @@
import { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiText, EuiTitle } from '@elastic/eui';
import React from 'react';
import { pure } from 'recompose';
-import styled from 'styled-components';
+import styled, { css } from 'styled-components';
import { InspectButton } from '../inspect';
const Header = styled.header<{ border?: boolean }>`
- ${props => `
+ ${props => css`
margin-bottom: ${props.theme.eui.euiSizeL};
${props.border &&
diff --git a/x-pack/legacy/plugins/siem/public/components/page/add_to_kql/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/add_to_kql/index.test.tsx
index 3888ab365195..6f4f7d953cb8 100644
--- a/x-pack/legacy/plugins/siem/public/components/page/add_to_kql/index.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/page/add_to_kql/index.test.tsx
@@ -87,6 +87,7 @@ describe('AddToKql Component', async () => {
expect(store.getState().hosts.page).toEqual({
queries: {
authentications: {
+ activePage: 0,
limit: 10,
},
hosts: {
diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/__snapshots__/index.test.tsx.snap
index 6d8e0475fa2b..23392c5880f7 100644
--- a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/__snapshots__/index.test.tsx.snap
+++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/__snapshots__/index.test.tsx.snap
@@ -100,12 +100,12 @@ exports[`Authentication Table Component rendering it renders the authentication
},
]
}
- hasNextPage={true}
+ fakeTotalCount={50}
id="authentication"
- loadMore={[MockFunction]}
+ loadPage={[MockFunction]}
loading={false}
- nextCursor="aa7ca589f1b8220002f2fc61c64cfbf1"
- totalCount={4}
+ showMorePagesIndicator={true}
+ totalCount={54}
type="page"
/>
`;
diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx
index 02a9b26a998d..360f6b9c6dfd 100644
--- a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx
@@ -18,7 +18,7 @@ import * as i18n from './translations';
import { AuthenticationTable, getAuthenticationColumnsCurated } from '.';
describe('Authentication Table Component', () => {
- const loadMore = jest.fn();
+ const loadPage = jest.fn();
const state: State = mockGlobalState;
let store = createStore(state, apolloClientObservable);
@@ -33,11 +33,15 @@ describe('Authentication Table Component', () => {
diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.tsx
index da78e9a71982..532f126c6b16 100644
--- a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/index.tsx
@@ -19,21 +19,23 @@ import { DragEffects, DraggableWrapper } from '../../../drag_and_drop/draggable_
import { escapeDataProviderId } from '../../../drag_and_drop/helpers';
import { getEmptyTagValue } from '../../../empty_value';
import { HostDetailsLink, IPDetailsLink } from '../../../links';
-import { Columns, ItemsPerRow, LoadMoreTable } from '../../../load_more_table';
+import { Columns, ItemsPerRow, PaginatedTable } from '../../../paginated_table';
import { IS_OPERATOR } from '../../../timeline/data_providers/data_provider';
import { Provider } from '../../../timeline/data_providers/provider';
import * as i18n from './translations';
import { getRowItemDraggables } from '../../../tables/helpers';
+const tableType = hostsModel.HostsTableType.authentications;
+
interface OwnProps {
data: AuthenticationsEdges[];
+ fakeTotalCount: number;
loading: boolean;
+ loadPage: (newActivePage: number) => void;
id: string;
- hasNextPage: boolean;
- nextCursor: string;
+ showMorePagesIndicator: boolean;
totalCount: number;
- loadMore: (cursor: string) => void;
type: hostsModel.HostsType;
}
@@ -43,8 +45,30 @@ interface AuthenticationTableReduxProps {
interface AuthenticationTableDispatchProps {
updateLimitPagination: ActionCreator<{ limit: number; hostsType: hostsModel.HostsType }>;
+ updateTableActivePage: ActionCreator<{
+ activePage: number;
+ hostsType: hostsModel.HostsType;
+ tableType: hostsModel.HostsTableType;
+ }>;
+ updateTableLimit: ActionCreator<{
+ limit: number;
+ hostsType: hostsModel.HostsType;
+ tableType: hostsModel.HostsTableType;
+ }>;
}
+export declare type AuthTableColumns = [
+ Columns,
+ Columns,
+ Columns,
+ Columns,
+ Columns,
+ Columns,
+ Columns,
+ Columns,
+ Columns
+];
+
type AuthenticationTableProps = OwnProps &
AuthenticationTableReduxProps &
AuthenticationTableDispatchProps;
@@ -58,32 +82,24 @@ const rowItems: ItemsPerRow[] = [
text: i18n.ROWS_10,
numberOfRow: 10,
},
- {
- text: i18n.ROWS_20,
- numberOfRow: 20,
- },
- {
- text: i18n.ROWS_50,
- numberOfRow: 50,
- },
];
const AuthenticationTableComponent = pure(
({
+ fakeTotalCount,
data,
- hasNextPage,
id,
limit,
loading,
- loadMore,
+ loadPage,
+ showMorePagesIndicator,
totalCount,
- nextCursor,
- updateLimitPagination,
type,
+ updateTableActivePage,
+ updateTableLimit,
}) => (
- (
limit={limit}
loading={loading}
loadingTitle={i18n.AUTHENTICATIONS}
- loadMore={() => loadMore(nextCursor)}
+ loadPage={newActivePage => loadPage(newActivePage)}
pageOfItems={data}
+ showMorePagesIndicator={showMorePagesIndicator}
+ totalCount={fakeTotalCount}
updateLimitPagination={newLimit =>
- updateLimitPagination({ limit: newLimit, hostsType: type })
+ updateTableLimit({
+ hostsType: type,
+ limit: newLimit,
+ tableType,
+ })
+ }
+ updateActivePage={newPage =>
+ updateTableActivePage({
+ activePage: newPage,
+ hostsType: type,
+ tableType,
+ })
}
+ updateProps={{ totalCount }}
/>
)
);
@@ -112,20 +142,12 @@ export const AuthenticationTable = connect(
makeMapStateToProps,
{
updateLimitPagination: hostsActions.updateAuthenticationsLimit,
+ updateTableActivePage: hostsActions.updateTableActivePage,
+ updateTableLimit: hostsActions.updateTableLimit,
}
)(AuthenticationTableComponent);
-const getAuthenticationColumns = (): [
- Columns,
- Columns,
- Columns,
- Columns,
- Columns,
- Columns,
- Columns,
- Columns,
- Columns
-] => [
+const getAuthenticationColumns = (): AuthTableColumns => [
{
name: i18n.USER,
truncateText: false,
diff --git a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/mock.ts b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/mock.ts
index 29d1a3ce9737..50a1fa8eb7d7 100644
--- a/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/mock.ts
+++ b/x-pack/legacy/plugins/siem/public/components/page/hosts/authentications_table/mock.ts
@@ -8,7 +8,7 @@ import { AuthenticationsData } from '../../../../graphql/types';
export const mockData: { Authentications: AuthenticationsData } = {
Authentications: {
- totalCount: 4,
+ totalCount: 54,
edges: [
{
node: {
@@ -74,10 +74,9 @@ export const mockData: { Authentications: AuthenticationsData } = {
},
],
pageInfo: {
- endCursor: {
- value: 'aa7ca589f1b8220002f2fc61c64cfbf1',
- },
- hasNextPage: true,
+ activePage: 1,
+ fakeTotalCount: 50,
+ showMorePagesIndicator: true,
},
},
};
diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/__snapshots__/index.test.tsx.snap b/x-pack/legacy/plugins/siem/public/components/paginated_table/__snapshots__/index.test.tsx.snap
new file mode 100644
index 000000000000..fa30c0895d45
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/paginated_table/__snapshots__/index.test.tsx.snap
@@ -0,0 +1,107 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Paginated Table Component rendering it renders the default load more table 1`] = `
+
+ My test supplement.
+
+ }
+ headerTitle="Hosts"
+ headerTooltip="My test tooltip"
+ headerUnit="Test Unit"
+ itemsPerRow={
+ Array [
+ Object {
+ "numberOfRow": 2,
+ "text": "2 rows",
+ },
+ Object {
+ "numberOfRow": 5,
+ "text": "5 rows",
+ },
+ Object {
+ "numberOfRow": 10,
+ "text": "10 rows",
+ },
+ Object {
+ "numberOfRow": 20,
+ "text": "20 rows",
+ },
+ Object {
+ "numberOfRow": 50,
+ "text": "50 rows",
+ },
+ ]
+ }
+ limit={1}
+ loadPage={[Function]}
+ loading={false}
+ loadingTitle="Hosts"
+ pageOfItems={
+ Array [
+ Object {
+ "cursor": Object {
+ "value": "98966fa2013c396155c460d35c0902be",
+ },
+ "host": Object {
+ "_id": "cPsuhGcB0WOhS6qyTKC0",
+ "firstSeen": "2018-12-06T15:40:53.319Z",
+ "name": "elrond.elstc.co",
+ "os": "Ubuntu",
+ "version": "18.04.1 LTS (Bionic Beaver)",
+ },
+ },
+ Object {
+ "cursor": Object {
+ "value": "aa7ca589f1b8220002f2fc61c64cfbf1",
+ },
+ "host": Object {
+ "_id": "KwQDiWcB0WOhS6qyXmrW",
+ "firstSeen": "2018-12-07T14:12:38.560Z",
+ "name": "siem-kibana",
+ "os": "Debian GNU/Linux",
+ "version": "9 (stretch)",
+ },
+ },
+ ]
+ }
+ showMorePagesIndicator={true}
+ totalCount={10}
+ updateActivePage={[Function]}
+ updateLimitPagination={[Function]}
+/>
+`;
diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/helpers.test.ts b/x-pack/legacy/plugins/siem/public/components/paginated_table/helpers.test.ts
new file mode 100644
index 000000000000..f3a5b87fc5a3
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/paginated_table/helpers.test.ts
@@ -0,0 +1,45 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { generateTablePaginationOptions } from './helpers';
+
+describe('generateTablePaginationOptions pagination helper function', () => {
+ let activePage;
+ let limit;
+ test('generates 5 pages when activePage 0', () => {
+ activePage = 0;
+ limit = 10;
+ const result = generateTablePaginationOptions(activePage, limit);
+ expect(result).toEqual({
+ activePage,
+ cursorStart: 0,
+ fakePossibleCount: 50,
+ querySize: 10,
+ });
+ });
+ test('generates 6 pages when activePage 4', () => {
+ activePage = 4;
+ limit = 10;
+ const result = generateTablePaginationOptions(activePage, limit);
+ expect(result).toEqual({
+ activePage,
+ cursorStart: 40,
+ fakePossibleCount: 60,
+ querySize: 50,
+ });
+ });
+ test('generates 5 pages when activePage 2', () => {
+ activePage = 2;
+ limit = 10;
+ const result = generateTablePaginationOptions(activePage, limit);
+ expect(result).toEqual({
+ activePage,
+ cursorStart: 20,
+ fakePossibleCount: 50,
+ querySize: 30,
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/helpers.ts b/x-pack/legacy/plugins/siem/public/components/paginated_table/helpers.ts
new file mode 100644
index 000000000000..c63b8699e79e
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/paginated_table/helpers.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { PaginationInputPaginated } from '../../graphql/types';
+
+export const generateTablePaginationOptions = (
+ activePage: number,
+ limit: number
+): PaginationInputPaginated => {
+ const cursorStart = activePage * limit;
+ return {
+ activePage,
+ cursorStart,
+ fakePossibleCount: 4 <= activePage && activePage > 0 ? limit * (activePage + 2) : limit * 5,
+ querySize: limit + cursorStart,
+ };
+};
diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.mock.tsx b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.mock.tsx
new file mode 100644
index 000000000000..513747c70036
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.mock.tsx
@@ -0,0 +1,118 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { getOrEmptyTagFromValue } from '../empty_value';
+
+import { Columns, ItemsPerRow } from './index';
+
+export const mockData = {
+ Hosts: {
+ totalCount: 4,
+ edges: [
+ {
+ host: {
+ _id: 'cPsuhGcB0WOhS6qyTKC0',
+ name: 'elrond.elstc.co',
+ os: 'Ubuntu',
+ version: '18.04.1 LTS (Bionic Beaver)',
+ firstSeen: '2018-12-06T15:40:53.319Z',
+ },
+ cursor: {
+ value: '98966fa2013c396155c460d35c0902be',
+ },
+ },
+ {
+ host: {
+ _id: 'KwQDiWcB0WOhS6qyXmrW',
+ name: 'siem-kibana',
+ os: 'Debian GNU/Linux',
+ version: '9 (stretch)',
+ firstSeen: '2018-12-07T14:12:38.560Z',
+ },
+ cursor: {
+ value: 'aa7ca589f1b8220002f2fc61c64cfbf1',
+ },
+ },
+ ],
+ pageInfo: {
+ activePage: 0,
+ endCursor: {
+ value: 'aa7ca589f1b8220002f2fc61c64cfbf1',
+ },
+ },
+ },
+};
+
+export const getHostsColumns = (): [
+ Columns,
+ Columns,
+ Columns,
+ Columns
+] => [
+ {
+ field: 'node.host.name',
+ name: 'Host',
+ truncateText: false,
+ hideForMobile: false,
+ render: (name: string) => getOrEmptyTagFromValue(name),
+ },
+ {
+ field: 'node.host.firstSeen',
+ name: 'First seen',
+ truncateText: false,
+ hideForMobile: false,
+ render: (firstSeen: string) => getOrEmptyTagFromValue(firstSeen),
+ },
+ {
+ field: 'node.host.os',
+ name: 'OS',
+ truncateText: false,
+ hideForMobile: false,
+ render: (os: string) => getOrEmptyTagFromValue(os),
+ },
+ {
+ field: 'node.host.version',
+ name: 'Version',
+ truncateText: false,
+ hideForMobile: false,
+ render: (version: string) => getOrEmptyTagFromValue(version),
+ },
+];
+
+export const sortedHosts: [
+ Columns,
+ Columns,
+ Columns,
+ Columns
+] = getHostsColumns().map(h => ({ ...h, sortable: true })) as [
+ Columns,
+ Columns,
+ Columns,
+ Columns
+];
+
+export const rowItems: ItemsPerRow[] = [
+ {
+ text: '2 rows',
+ numberOfRow: 2,
+ },
+ {
+ text: '5 rows',
+ numberOfRow: 5,
+ },
+ {
+ text: '10 rows',
+ numberOfRow: 10,
+ },
+ {
+ text: '20 rows',
+ numberOfRow: 20,
+ },
+ {
+ text: '50 rows',
+ numberOfRow: 50,
+ },
+];
diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.test.tsx
new file mode 100644
index 000000000000..609aec0a2e81
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.test.tsx
@@ -0,0 +1,517 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { mount, shallow } from 'enzyme';
+import toJson from 'enzyme-to-json';
+import * as React from 'react';
+
+import { Direction } from '../../graphql/types';
+
+import { BasicTableProps, PaginatedTable } from './index';
+import { getHostsColumns, mockData, rowItems, sortedHosts } from './index.mock';
+import { ThemeProvider } from 'styled-components';
+import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json';
+import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants';
+
+jest.mock('react', () => {
+ const r = jest.requireActual('react');
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ return { ...r, memo: (x: any) => x };
+});
+
+describe('Paginated Table Component', () => {
+ const theme = () => ({ eui: euiDarkVars, darkMode: true });
+ let loadPage: jest.Mock;
+ let updateLimitPagination: jest.Mock;
+ let updateActivePage: jest.Mock;
+ beforeEach(() => {
+ loadPage = jest.fn();
+ updateLimitPagination = jest.fn();
+ updateActivePage = jest.fn();
+ });
+
+ describe('rendering', () => {
+ test('it renders the default load more table', () => {
+ const wrapper = shallow(
+
+ {'My test supplement.'}}
+ headerTitle="Hosts"
+ headerTooltip="My test tooltip"
+ headerUnit="Test Unit"
+ itemsPerRow={rowItems}
+ limit={1}
+ loading={false}
+ loadingTitle="Hosts"
+ loadPage={newActivePage => loadPage(newActivePage)}
+ pageOfItems={mockData.Hosts.edges}
+ showMorePagesIndicator={true}
+ totalCount={10}
+ updateActivePage={activePage => updateActivePage(activePage)}
+ updateLimitPagination={limit => updateLimitPagination({ limit })}
+ />
+
+ );
+
+ expect(toJson(wrapper)).toMatchSnapshot();
+ });
+
+ test('it renders the loading panel at the beginning ', () => {
+ const wrapper = mount(
+
+ {'My test supplement.'}}
+ headerTitle="Hosts"
+ headerTooltip="My test tooltip"
+ headerUnit="Test Unit"
+ itemsPerRow={rowItems}
+ limit={1}
+ loading={true}
+ loadingTitle="Hosts"
+ loadPage={newActivePage => loadPage(newActivePage)}
+ pageOfItems={[]}
+ showMorePagesIndicator={true}
+ totalCount={10}
+ updateActivePage={activePage => updateActivePage(activePage)}
+ updateLimitPagination={limit => updateLimitPagination({ limit })}
+ />
+
+ );
+
+ expect(
+ wrapper.find('[data-test-subj="InitialLoadingPanelPaginatedTable"]').exists()
+ ).toBeTruthy();
+ });
+
+ test('it renders the over loading panel after data has been in the table ', () => {
+ const wrapper = mount(
+
+ {'My test supplement.'}}
+ headerTitle="Hosts"
+ headerTooltip="My test tooltip"
+ headerUnit="Test Unit"
+ itemsPerRow={rowItems}
+ limit={1}
+ loading={true}
+ loadingTitle="Hosts"
+ loadPage={newActivePage => loadPage(newActivePage)}
+ pageOfItems={mockData.Hosts.edges}
+ showMorePagesIndicator={true}
+ totalCount={10}
+ updateActivePage={activePage => updateActivePage(activePage)}
+ updateLimitPagination={limit => updateLimitPagination({ limit })}
+ />
+
+ );
+
+ expect(wrapper.find('[data-test-subj="LoadingPanelPaginatedTable"]').exists()).toBeTruthy();
+ });
+
+ test('it renders the correct amount of pages and starts at activePage: 0', () => {
+ const wrapper = mount(
+
+ {'My test supplement.'}}
+ headerTitle="Hosts"
+ headerTooltip="My test tooltip"
+ headerUnit="Test Unit"
+ itemsPerRow={rowItems}
+ limit={1}
+ loading={false}
+ loadingTitle="Hosts"
+ loadPage={newActivePage => loadPage(newActivePage)}
+ pageOfItems={mockData.Hosts.edges}
+ showMorePagesIndicator={true}
+ totalCount={10}
+ updateActivePage={updateActivePage}
+ updateLimitPagination={limit => updateLimitPagination({ limit })}
+ />
+
+ );
+
+ const paginiationProps = wrapper
+ .find('[data-test-subj="numberedPagination"]')
+ .first()
+ .props();
+
+ const expectedPaginationProps = {
+ 'data-test-subj': 'numberedPagination',
+ pageCount: 10,
+ activePage: 0,
+ };
+ expect(JSON.stringify(paginiationProps)).toEqual(JSON.stringify(expectedPaginationProps));
+ });
+
+ test('it render popover to select new limit in table', () => {
+ const wrapper = mount(
+
+ {'My test supplement.'}}
+ headerTitle="Hosts"
+ headerTooltip="My test tooltip"
+ headerUnit="Test Unit"
+ itemsPerRow={rowItems}
+ limit={2}
+ loading={false}
+ loadingTitle="Hosts"
+ loadPage={newActivePage => loadPage(newActivePage)}
+ pageOfItems={mockData.Hosts.edges}
+ showMorePagesIndicator={true}
+ totalCount={10}
+ updateActivePage={activePage => updateActivePage(activePage)}
+ updateLimitPagination={limit => updateLimitPagination({ limit })}
+ />
+
+ );
+
+ wrapper
+ .find('[data-test-subj="loadingMoreSizeRowPopover"] button')
+ .first()
+ .simulate('click');
+ expect(wrapper.find('[data-test-subj="loadingMorePickSizeRow"]').exists()).toBeTruthy();
+ });
+
+ test('it will NOT render popover to select new limit in table if props itemsPerRow is empty', () => {
+ const wrapper = mount(
+
+ {'My test supplement.'}}
+ headerTitle="Hosts"
+ headerTooltip="My test tooltip"
+ headerUnit="Test Unit"
+ itemsPerRow={[]}
+ limit={2}
+ loading={false}
+ loadingTitle="Hosts"
+ loadPage={newActivePage => loadPage(newActivePage)}
+ pageOfItems={mockData.Hosts.edges}
+ showMorePagesIndicator={true}
+ totalCount={10}
+ updateActivePage={activePage => updateActivePage(activePage)}
+ updateLimitPagination={limit => updateLimitPagination({ limit })}
+ />
+
+ );
+
+ expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeFalsy();
+ });
+
+ test('It should render a sort icon if sorting is defined', () => {
+ const mockOnChange = jest.fn();
+ const wrapper = mount(
+
+ {'My test supplement.'}}
+ headerTitle="Hosts"
+ headerTooltip="My test tooltip"
+ headerUnit="Test Unit"
+ itemsPerRow={rowItems}
+ limit={2}
+ loading={false}
+ loadingTitle="Hosts"
+ loadPage={jest.fn()}
+ onChange={mockOnChange}
+ pageOfItems={mockData.Hosts.edges}
+ showMorePagesIndicator={true}
+ sorting={{ direction: Direction.asc, field: 'node.host.name' }}
+ totalCount={10}
+ updateActivePage={activePage => updateActivePage(activePage)}
+ updateLimitPagination={limit => updateLimitPagination({ limit })}
+ />
+
+ );
+
+ expect(wrapper.find('.euiTable thead tr th button svg')).toBeTruthy();
+ });
+
+ test('Should display toast when user reaches end of results max', () => {
+ const wrapper = mount(
+
+ {'My test supplement.'}}
+ headerTitle="Hosts"
+ headerTooltip="My test tooltip"
+ headerUnit="Test Unit"
+ itemsPerRow={rowItems}
+ limit={DEFAULT_MAX_TABLE_QUERY_SIZE}
+ loading={false}
+ loadingTitle="Hosts"
+ loadPage={newActivePage => loadPage(newActivePage)}
+ pageOfItems={mockData.Hosts.edges}
+ showMorePagesIndicator={true}
+ totalCount={DEFAULT_MAX_TABLE_QUERY_SIZE * 3}
+ updateActivePage={activePage => updateActivePage(activePage)}
+ updateLimitPagination={limit => updateLimitPagination({ limit })}
+ />
+
+ );
+ wrapper
+ .find('[data-test-subj="pagination-button-next"]')
+ .first()
+ .simulate('click');
+ expect(updateActivePage.mock.calls.length).toEqual(0);
+ });
+
+ test('Should show items per row if totalCount is greater than items', () => {
+ const wrapper = mount(
+
+ {'My test supplement.'}}
+ headerTitle="Hosts"
+ headerTooltip="My test tooltip"
+ headerUnit="Test Unit"
+ itemsPerRow={rowItems}
+ limit={DEFAULT_MAX_TABLE_QUERY_SIZE}
+ loading={false}
+ loadingTitle="Hosts"
+ loadPage={newActivePage => loadPage(newActivePage)}
+ pageOfItems={mockData.Hosts.edges}
+ showMorePagesIndicator={true}
+ totalCount={30}
+ updateActivePage={activePage => updateActivePage(activePage)}
+ updateLimitPagination={limit => updateLimitPagination({ limit })}
+ />
+
+ );
+ expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeTruthy();
+ });
+
+ test('Should hide items per row if totalCount is less than items', () => {
+ const wrapper = mount(
+
+ {'My test supplement.'}}
+ headerTitle="Hosts"
+ headerTooltip="My test tooltip"
+ headerUnit="Test Unit"
+ itemsPerRow={rowItems}
+ limit={DEFAULT_MAX_TABLE_QUERY_SIZE}
+ loading={false}
+ loadingTitle="Hosts"
+ loadPage={newActivePage => loadPage(newActivePage)}
+ pageOfItems={mockData.Hosts.edges}
+ showMorePagesIndicator={true}
+ totalCount={1}
+ updateActivePage={activePage => updateActivePage(activePage)}
+ updateLimitPagination={limit => updateLimitPagination({ limit })}
+ />
+
+ );
+ expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeFalsy();
+ });
+ });
+
+ describe('Events', () => {
+ test('should call updateActivePage with 1 when clicking to the first page', () => {
+ const wrapper = mount(
+
+ {'My test supplement.'}}
+ headerTitle="Hosts"
+ headerTooltip="My test tooltip"
+ headerUnit="Test Unit"
+ itemsPerRow={rowItems}
+ limit={1}
+ loading={false}
+ loadingTitle="Hosts"
+ loadPage={newActivePage => loadPage(newActivePage)}
+ pageOfItems={mockData.Hosts.edges}
+ showMorePagesIndicator={true}
+ totalCount={10}
+ updateActivePage={activePage => updateActivePage(activePage)}
+ updateLimitPagination={limit => updateLimitPagination({ limit })}
+ />
+
+ );
+ wrapper
+ .find('[data-test-subj="pagination-button-next"]')
+ .first()
+ .simulate('click');
+ expect(updateActivePage.mock.calls[0][0]).toEqual(1);
+ });
+
+ test('Should call updateActivePage with 0 when you pick a new limit', () => {
+ const wrapper = mount(
+
+ {'My test supplement.'}}
+ headerTitle="Hosts"
+ headerTooltip="My test tooltip"
+ headerUnit="Test Unit"
+ itemsPerRow={rowItems}
+ limit={2}
+ loading={false}
+ loadingTitle="Hosts"
+ loadPage={newActivePage => loadPage(newActivePage)}
+ pageOfItems={mockData.Hosts.edges}
+ showMorePagesIndicator={true}
+ totalCount={10}
+ updateActivePage={activePage => updateActivePage(activePage)}
+ updateLimitPagination={limit => updateLimitPagination({ limit })}
+ />
+
+ );
+ wrapper
+ .find('[data-test-subj="pagination-button-next"]')
+ .first()
+ .simulate('click');
+
+ wrapper
+ .find('[data-test-subj="loadingMoreSizeRowPopover"] button')
+ .first()
+ .simulate('click');
+
+ wrapper
+ .find('[data-test-subj="loadingMorePickSizeRow"] button')
+ .first()
+ .simulate('click');
+ expect(updateActivePage.mock.calls[1][0]).toEqual(0);
+ });
+
+ test('should call updateActivePage with 0 when an update prop changes', () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const ourProps: BasicTableProps = {
+ columns: getHostsColumns(),
+ headerCount: 1,
+ headerSupplement: {'My test supplement.'}
,
+ headerTitle: 'Hosts',
+ headerTooltip: 'My test tooltip',
+ headerUnit: 'Test Unit',
+ itemsPerRow: rowItems,
+ limit: 1,
+ loading: false,
+ loadingTitle: 'Hosts',
+ loadPage: newActivePage => loadPage(newActivePage),
+ pageOfItems: mockData.Hosts.edges,
+ showMorePagesIndicator: true,
+ totalCount: 10,
+ updateActivePage: activePage => updateActivePage(activePage),
+ updateLimitPagination: limit => updateLimitPagination({ limit }),
+ updateProps: { isThisAwesome: false },
+ };
+
+ // enzyme does not allow us to pass props to child of HOC
+ // so we make a component to pass it the props context
+ // ComponentWithContext will pass the changed props to Component
+ // https://github.com/airbnb/enzyme/issues/1853#issuecomment-443475903
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const ComponentWithContext = (props: BasicTableProps) => {
+ return (
+
+
+
+ );
+ };
+
+ const wrapper = mount();
+ wrapper
+ .find('[data-test-subj="pagination-button-next"]')
+ .first()
+ .simulate('click');
+ wrapper.setProps({ updateProps: { isThisAwesome: true } });
+ expect(updateActivePage.mock.calls[1][0]).toEqual(0);
+ });
+
+ test('Should call updateLimitPagination when you pick a new limit', () => {
+ const wrapper = mount(
+
+ {'My test supplement.'}}
+ headerTitle="Hosts"
+ headerTooltip="My test tooltip"
+ headerUnit="Test Unit"
+ itemsPerRow={rowItems}
+ limit={2}
+ loading={false}
+ loadingTitle="Hosts"
+ loadPage={newActivePage => loadPage(newActivePage)}
+ pageOfItems={mockData.Hosts.edges}
+ showMorePagesIndicator={true}
+ totalCount={10}
+ updateActivePage={activePage => updateActivePage(activePage)}
+ updateLimitPagination={limit => updateLimitPagination({ limit })}
+ />
+
+ );
+
+ wrapper
+ .find('[data-test-subj="loadingMoreSizeRowPopover"] button')
+ .first()
+ .simulate('click');
+
+ wrapper
+ .find('[data-test-subj="loadingMorePickSizeRow"] button')
+ .first()
+ .simulate('click');
+ expect(updateLimitPagination).toBeCalled();
+ });
+
+ test('Should call onChange when you choose a new sort in the table', () => {
+ const mockOnChange = jest.fn();
+ const wrapper = mount(
+
+ {'My test supplement.'}}
+ headerTitle="Hosts"
+ headerTooltip="My test tooltip"
+ headerUnit="Test Unit"
+ itemsPerRow={rowItems}
+ limit={2}
+ loading={false}
+ loadingTitle="Hosts"
+ loadPage={jest.fn()}
+ onChange={mockOnChange}
+ pageOfItems={mockData.Hosts.edges}
+ showMorePagesIndicator={true}
+ sorting={{ direction: Direction.asc, field: 'node.host.name' }}
+ totalCount={10}
+ updateActivePage={activePage => updateActivePage(activePage)}
+ updateLimitPagination={limit => updateLimitPagination({ limit })}
+ />
+
+ );
+
+ wrapper
+ .find('.euiTable thead tr th button')
+ .first()
+ .simulate('click');
+
+ expect(mockOnChange).toBeCalled();
+ expect(mockOnChange.mock.calls[0]).toEqual([
+ { page: undefined, sort: { direction: 'desc', field: 'node.host.name' } },
+ ]);
+ });
+ });
+});
diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx
new file mode 100644
index 000000000000..0ad5f6daa815
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/paginated_table/index.tsx
@@ -0,0 +1,333 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+import {
+ EuiBasicTable,
+ EuiButtonEmpty,
+ EuiContextMenuItem,
+ EuiContextMenuPanel,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiGlobalToastListToast as Toast,
+ EuiPagination,
+ EuiPanel,
+ EuiPopover,
+} from '@elastic/eui';
+import { isEmpty, noop, getOr } from 'lodash/fp';
+import React, { memo, useState, useEffect } from 'react';
+import styled from 'styled-components';
+
+import { Direction } from '../../graphql/types';
+import { AuthTableColumns } from '../page/hosts/authentications_table';
+import { HeaderPanel } from '../header_panel';
+import { LoadingPanel } from '../loading';
+import { useStateToaster } from '../toasters';
+import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants';
+
+import * as i18n from './translations';
+
+const DEFAULT_DATA_TEST_SUBJ = 'paginated-table';
+
+export interface ItemsPerRow {
+ text: string;
+ numberOfRow: number;
+}
+
+export interface SortingBasicTable {
+ field: string;
+ direction: Direction;
+ allowNeutralSort?: boolean;
+}
+
+export interface Criteria {
+ page?: { index: number; size: number };
+ sort?: SortingBasicTable;
+}
+
+declare type HostsTableColumns = [
+ Columns,
+ Columns,
+ Columns,
+ Columns
+];
+
+declare type BasicTableColumns = AuthTableColumns | HostsTableColumns;
+
+declare type SiemTables = BasicTableProps;
+
+// Using telescoping templates to remove 'any' that was polluting downstream column type checks
+export interface BasicTableProps {
+ columns: T;
+ dataTestSubj?: string;
+ headerCount: number;
+ headerSupplement?: React.ReactElement;
+ headerTitle: string | React.ReactElement;
+ headerTooltip?: string;
+ headerUnit: string | React.ReactElement;
+ id?: string;
+ itemsPerRow?: ItemsPerRow[];
+ limit: number;
+ loading: boolean;
+ loadingTitle?: string;
+ loadPage: (activePage: number) => void;
+ onChange?: (criteria: Criteria) => void;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ pageOfItems: any[];
+ showMorePagesIndicator: boolean;
+ sorting?: SortingBasicTable;
+ totalCount: number;
+ updateActivePage: (activePage: number) => void;
+ updateLimitPagination: (limit: number) => void;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ updateProps?: { [key: string]: any };
+}
+
+export interface Columns {
+ field?: string;
+ name: string | React.ReactNode;
+ isMobileHeader?: boolean;
+ sortable?: boolean;
+ truncateText?: boolean;
+ hideForMobile?: boolean;
+ render?: (item: T) => void;
+ width?: string;
+}
+
+export const PaginatedTable = memo(
+ ({
+ columns,
+ dataTestSubj = DEFAULT_DATA_TEST_SUBJ,
+ headerCount,
+ headerSupplement,
+ headerTitle,
+ headerTooltip,
+ headerUnit,
+ id,
+ itemsPerRow,
+ limit,
+ loading,
+ loadingTitle,
+ loadPage,
+ onChange = noop,
+ pageOfItems,
+ showMorePagesIndicator,
+ sorting = null,
+ totalCount,
+ updateActivePage,
+ updateLimitPagination,
+ updateProps,
+ }) => {
+ const [activePage, setActivePage] = useState(0);
+ const [showInspect, setShowInspect] = useState(false);
+ const [isEmptyTable, setEmptyTable] = useState(pageOfItems.length === 0);
+ const [isPopoverOpen, setPopoverOpen] = useState(false);
+ const pageCount = Math.ceil(totalCount / limit);
+ const dispatchToaster = useStateToaster()[1];
+ const effectDeps = updateProps ? [limit, ...Object.values(updateProps)] : [limit];
+ useEffect(() => {
+ if (activePage !== 0) {
+ setActivePage(0);
+ updateActivePage(0);
+ }
+ }, effectDeps);
+
+ const onButtonClick = () => {
+ setPopoverOpen(!isPopoverOpen);
+ };
+
+ const closePopover = () => {
+ setPopoverOpen(false);
+ };
+
+ const goToPage = (newActivePage: number) => {
+ if ((newActivePage + 1) * limit >= DEFAULT_MAX_TABLE_QUERY_SIZE) {
+ const toast: Toast = {
+ id: 'PaginationWarningMsg',
+ title: headerTitle + i18n.TOAST_TITLE,
+ color: 'warning',
+ iconType: 'alert',
+ toastLifeTimeMs: 10000,
+ text: i18n.TOAST_TEXT,
+ };
+ return dispatchToaster({
+ type: 'addToaster',
+ toast,
+ });
+ }
+ setActivePage(newActivePage);
+ loadPage(newActivePage);
+ updateActivePage(newActivePage);
+ };
+ if (!isEmpty(pageOfItems) && isEmptyTable) {
+ setEmptyTable(false);
+ }
+ if (loading && isEmptyTable) {
+ return (
+
+
+
+ );
+ }
+
+ const button = (
+
+ {`${i18n.ROWS}: ${limit}`}
+
+ );
+
+ const rowItems =
+ itemsPerRow &&
+ itemsPerRow.map((item: ItemsPerRow) => (
+ {
+ closePopover();
+ updateLimitPagination(item.numberOfRow);
+ updateActivePage(0); // reset results to first page
+ }}
+ >
+ {item.text}
+
+ ));
+ const PaginationWrapper = showMorePagesIndicator ? PaginationEuiFlexItem : EuiFlexItem;
+ return (
+ setShowInspect(true)}
+ onMouseLeave={() => setShowInspect(false)}
+ >
+
+ {loading && (
+ <>
+
+
+ >
+ )}
+
+
+ {headerSupplement}
+
+
+
+
+
+
+ {itemsPerRow && itemsPerRow.length > 0 && totalCount >= itemsPerRow[0].numberOfRow && (
+
+ )}
+
+
+
+
+
+
+
+
+ );
+ }
+);
+
+export const BasicTableContainer = styled.div`
+ position: relative;
+`;
+
+const FooterAction = styled.div`
+ margin-top: 0.5rem;
+ width: 100%;
+`;
+
+/*
+ * The getOr is just there to simplify the test
+ * So we do NOT need to wrap it around TestProvider
+ */
+const BackgroundRefetch = styled.div`
+ background-color: ${props => getOr('#ffffff', 'theme.eui.euiColorLightShade', props)};
+ margin: -5px;
+ height: calc(100% + 10px);
+ opacity: 0.7;
+ width: calc(100% + 10px);
+ position: absolute;
+ z-index: 3;
+ border-radius: 5px;
+`;
+
+const BasicTable = styled(EuiBasicTable)`
+ tbody {
+ th,
+ td {
+ vertical-align: top;
+ }
+ }
+`;
+
+const PaginationEuiFlexItem = styled(EuiFlexItem)`
+ button.euiButtonIcon.euiButtonIcon--text {
+ margin-left: 20px;
+ }
+ .euiPagination {
+ position: relative;
+ }
+ .euiPagination::before {
+ content: '\\2026';
+ bottom: 5px;
+ color: ${props => props.theme.eui.euiButtonColorDisabled};
+ font-size: ${props => props.theme.eui.euiFontSizeS};
+ position: absolute;
+ right: 30px;
+ }
+`;
diff --git a/x-pack/legacy/plugins/siem/public/components/paginated_table/translations.ts b/x-pack/legacy/plugins/siem/public/components/paginated_table/translations.ts
new file mode 100644
index 000000000000..1b9faf83f502
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/components/paginated_table/translations.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+export const LOADING = i18n.translate('xpack.siem.loadingMoreTable.loadingDescription', {
+ defaultMessage: 'Loading…',
+});
+
+export const LOAD_MORE = i18n.translate('xpack.siem.loadingMoreTable.loadMoreDescription', {
+ defaultMessage: 'Load More',
+});
+
+export const SHOWING = i18n.translate('xpack.siem.loadingMoreTable.showing', {
+ defaultMessage: 'Showing',
+});
+
+export const ROWS = i18n.translate('xpack.siem.loadingMoreTable.rows', {
+ defaultMessage: 'Rows',
+});
+
+export const TOAST_TITLE = i18n.translate('xpack.siem.unableToLoadMoreResults.title', {
+ defaultMessage: ' - too many results',
+});
+
+export const TOAST_TEXT = i18n.translate('xpack.siem.unableToLoadMoreResults.text', {
+ defaultMessage: 'Narrow your query to better filter the results',
+});
diff --git a/x-pack/legacy/plugins/siem/public/containers/authentications/index.gql_query.ts b/x-pack/legacy/plugins/siem/public/containers/authentications/index.gql_query.ts
index f713e0f4421e..eee35730cfdb 100644
--- a/x-pack/legacy/plugins/siem/public/containers/authentications/index.gql_query.ts
+++ b/x-pack/legacy/plugins/siem/public/containers/authentications/index.gql_query.ts
@@ -10,7 +10,7 @@ export const authenticationsQuery = gql`
query GetAuthenticationsQuery(
$sourceId: ID!
$timerange: TimerangeInput!
- $pagination: PaginationInput!
+ $pagination: PaginationInputPaginated!
$filterQuery: String
$defaultIndex: [String!]!
$inspect: Boolean!
@@ -58,10 +58,9 @@ export const authenticationsQuery = gql`
}
}
pageInfo {
- endCursor {
- value
- }
- hasNextPage
+ activePage
+ fakeTotalCount
+ showMorePagesIndicator
}
inspect @include(if: $inspect) {
dsl
diff --git a/x-pack/legacy/plugins/siem/public/containers/authentications/index.tsx b/x-pack/legacy/plugins/siem/public/containers/authentications/index.tsx
index c74ec85deba7..c18eaf7942b1 100644
--- a/x-pack/legacy/plugins/siem/public/containers/authentications/index.tsx
+++ b/x-pack/legacy/plugins/siem/public/containers/authentications/index.tsx
@@ -11,10 +11,15 @@ import { connect } from 'react-redux';
import chrome from 'ui/chrome';
import { DEFAULT_INDEX_KEY } from '../../../common/constants';
-import { AuthenticationsEdges, GetAuthenticationsQuery, PageInfo } from '../../graphql/types';
+import {
+ AuthenticationsEdges,
+ GetAuthenticationsQuery,
+ PageInfoPaginated,
+} from '../../graphql/types';
import { hostsModel, hostsSelectors, inputsModel, State, inputsSelectors } from '../../store';
import { createFilter, getDefaultFetchPolicy } from '../helpers';
-import { QueryTemplate, QueryTemplateProps } from '../query_template';
+import { generateTablePaginationOptions } from '../../components/paginated_table/helpers';
+import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated';
import { authenticationsQuery } from './index.gql_query';
@@ -25,40 +30,42 @@ export interface AuthenticationArgs {
inspect: inputsModel.InspectQuery;
authentications: AuthenticationsEdges[];
totalCount: number;
- pageInfo: PageInfo;
+ pageInfo: PageInfoPaginated;
loading: boolean;
- loadMore: (cursor: string) => void;
+ loadPage: (newActivePage: number) => void;
refetch: inputsModel.Refetch;
}
-export interface OwnProps extends QueryTemplateProps {
+export interface OwnProps extends QueryTemplatePaginatedProps {
children: (args: AuthenticationArgs) => React.ReactNode;
type: hostsModel.HostsType;
}
export interface AuthenticationsComponentReduxProps {
+ activePage: number;
isInspected: boolean;
limit: number;
}
type AuthenticationsProps = OwnProps & AuthenticationsComponentReduxProps;
-class AuthenticationsComponentQuery extends QueryTemplate<
+class AuthenticationsComponentQuery extends QueryTemplatePaginated<
AuthenticationsProps,
GetAuthenticationsQuery.Query,
GetAuthenticationsQuery.Variables
> {
public render() {
const {
- id = ID,
- isInspected,
+ activePage,
children,
+ endDate,
filterQuery,
+ id = ID,
+ isInspected,
+ limit,
skip,
sourceId,
startDate,
- endDate,
- limit,
} = this.props;
return (
@@ -73,11 +80,7 @@ class AuthenticationsComponentQuery extends QueryTemplate<
from: startDate!,
to: endDate!,
},
- pagination: {
- limit,
- cursor: null,
- tiebreaker: null,
- },
+ pagination: generateTablePaginationOptions(activePage, limit),
filterQuery: createFilter(filterQuery),
defaultIndex: chrome.getUiSettingsClient().get(DEFAULT_INDEX_KEY),
inspect: isInspected,
@@ -86,12 +89,9 @@ class AuthenticationsComponentQuery extends QueryTemplate<
{({ data, loading, fetchMore, refetch }) => {
const authentications = getOr([], 'source.Authentications.edges', data);
this.setFetchMore(fetchMore);
- this.setFetchMoreOptions((newCursor: string) => ({
+ this.setFetchMoreOptions((newActivePage: number) => ({
variables: {
- pagination: {
- cursor: newCursor,
- limit: limit + parseInt(newCursor, 10),
- },
+ pagination: generateTablePaginationOptions(newActivePage, limit),
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) {
@@ -103,24 +103,21 @@ class AuthenticationsComponentQuery extends QueryTemplate<
...fetchMoreResult.source,
Authentications: {
...fetchMoreResult.source.Authentications,
- edges: [
- ...prev.source.Authentications.edges,
- ...fetchMoreResult.source.Authentications.edges,
- ],
+ edges: [...fetchMoreResult.source.Authentications.edges],
},
},
};
},
}));
return children({
+ authentications,
id,
inspect: getOr(null, 'source.Authentications.inspect', data),
- refetch,
loading,
- totalCount: getOr(0, 'source.Authentications.totalCount', data),
- authentications,
+ loadPage: this.wrappedLoadMore,
pageInfo: getOr({}, 'source.Authentications.pageInfo', data),
- loadMore: this.wrappedLoadMore,
+ refetch,
+ totalCount: getOr(0, 'source.Authentications.totalCount', data),
});
}}
diff --git a/x-pack/legacy/plugins/siem/public/containers/query_template_paginated.tsx b/x-pack/legacy/plugins/siem/public/containers/query_template_paginated.tsx
new file mode 100644
index 000000000000..00cd8d21fb4e
--- /dev/null
+++ b/x-pack/legacy/plugins/siem/public/containers/query_template_paginated.tsx
@@ -0,0 +1,59 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import { ApolloQueryResult } from 'apollo-client';
+import React from 'react';
+import { FetchMoreOptions, FetchMoreQueryOptions, OperationVariables } from 'react-apollo';
+
+import { ESQuery } from '../../common/typed_json';
+
+export interface QueryTemplatePaginatedProps {
+ id?: string;
+ endDate?: number;
+ filterQuery?: ESQuery | string;
+ skip?: boolean;
+ sourceId: string;
+ startDate?: number;
+}
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type FetchMoreOptionsArgs = FetchMoreQueryOptions &
+ FetchMoreOptions;
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type PromiseApolloQueryResult = Promise>;
+
+export class QueryTemplatePaginated<
+ T extends QueryTemplatePaginatedProps,
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ TData = any,
+ TVariables = OperationVariables
+> extends React.PureComponent {
+ private fetchMore!: (
+ fetchMoreOptions: FetchMoreOptionsArgs
+ ) => PromiseApolloQueryResult;
+
+ private fetchMoreOptions!: (newActivePage: number) => FetchMoreOptionsArgs;
+
+ public constructor(props: T) {
+ super(props);
+ }
+
+ public setFetchMore = (
+ val: (fetchMoreOptions: FetchMoreOptionsArgs) => PromiseApolloQueryResult
+ ) => {
+ this.fetchMore = val;
+ };
+
+ public setFetchMoreOptions = (
+ val: (newActivePage: number) => FetchMoreOptionsArgs
+ ) => {
+ this.fetchMoreOptions = val;
+ };
+
+ public wrappedLoadMore = (newActivePage: number) => {
+ return this.fetchMore(this.fetchMoreOptions(newActivePage));
+ };
+}
diff --git a/x-pack/legacy/plugins/siem/public/graphql/introspection.json b/x-pack/legacy/plugins/siem/public/graphql/introspection.json
index 1f5c3b604833..3b86740c73dc 100644
--- a/x-pack/legacy/plugins/siem/public/graphql/introspection.json
+++ b/x-pack/legacy/plugins/siem/public/graphql/introspection.json
@@ -670,7 +670,11 @@
"type": {
"kind": "NON_NULL",
"name": null,
- "ofType": { "kind": "INPUT_OBJECT", "name": "PaginationInput", "ofType": null }
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "PaginationInputPaginated",
+ "ofType": null
+ }
},
"defaultValue": null
},
@@ -2333,13 +2337,13 @@
},
{
"kind": "INPUT_OBJECT",
- "name": "PaginationInput",
+ "name": "PaginationInputPaginated",
"description": "",
"fields": null,
"inputFields": [
{
- "name": "limit",
- "description": "The limit parameter allows you to configure the maximum amount of items to be returned",
+ "name": "activePage",
+ "description": "The activePage parameter defines the page of results you want to fetch",
"type": {
"kind": "NON_NULL",
"name": null,
@@ -2348,15 +2352,33 @@
"defaultValue": null
},
{
- "name": "cursor",
- "description": "The cursor parameter defines the next result you want to fetch",
- "type": { "kind": "SCALAR", "name": "String", "ofType": null },
+ "name": "cursorStart",
+ "description": "The cursorStart parameter defines the start of the results to be displayed",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null }
+ },
"defaultValue": null
},
{
- "name": "tiebreaker",
- "description": "The tiebreaker parameter allow to be more precise to fetch the next item",
- "type": { "kind": "SCALAR", "name": "String", "ofType": null },
+ "name": "fakePossibleCount",
+ "description": "The fakePossibleCount parameter determines the total count in order to show 5 additional pages",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "querySize",
+ "description": "The querySize parameter is the number of items to be returned",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null }
+ },
"defaultValue": null
}
],
@@ -2408,7 +2430,7 @@
"type": {
"kind": "NON_NULL",
"name": null,
- "ofType": { "kind": "OBJECT", "name": "PageInfo", "ofType": null }
+ "ofType": { "kind": "OBJECT", "name": "PageInfoPaginated", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
@@ -2969,22 +2991,42 @@
},
{
"kind": "OBJECT",
- "name": "PageInfo",
+ "name": "PageInfoPaginated",
"description": "",
"fields": [
{
- "name": "endCursor",
+ "name": "activePage",
"description": "",
"args": [],
- "type": { "kind": "OBJECT", "name": "CursorType", "ofType": null },
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null }
+ },
"isDeprecated": false,
"deprecationReason": null
},
{
- "name": "hasNextPage",
+ "name": "fakeTotalCount",
"description": "",
"args": [],
- "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null },
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "showMorePagesIndicator",
+ "description": "",
+ "args": [],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null }
+ },
"isDeprecated": false,
"deprecationReason": null
}
@@ -3045,6 +3087,39 @@
"enumValues": null,
"possibleTypes": null
},
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "PaginationInput",
+ "description": "",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "limit",
+ "description": "The limit parameter allows you to configure the maximum amount of items to be returned",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": { "kind": "SCALAR", "name": "Float", "ofType": null }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "cursor",
+ "description": "The cursor parameter defines the next result you want to fetch",
+ "type": { "kind": "SCALAR", "name": "String", "ofType": null },
+ "defaultValue": null
+ },
+ {
+ "name": "tiebreaker",
+ "description": "The tiebreaker parameter allow to be more precise to fetch the next item",
+ "type": { "kind": "SCALAR", "name": "String", "ofType": null },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
{
"kind": "INPUT_OBJECT",
"name": "SortField",
@@ -5117,6 +5192,33 @@
"enumValues": null,
"possibleTypes": null
},
+ {
+ "kind": "OBJECT",
+ "name": "PageInfo",
+ "description": "",
+ "fields": [
+ {
+ "name": "endCursor",
+ "description": "",
+ "args": [],
+ "type": { "kind": "OBJECT", "name": "CursorType", "ofType": null },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "hasNextPage",
+ "description": "",
+ "args": [],
+ "type": { "kind": "SCALAR", "name": "Boolean", "ofType": null },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [],
+ "enumValues": null,
+ "possibleTypes": null
+ },
{
"kind": "OBJECT",
"name": "TimelineData",
diff --git a/x-pack/legacy/plugins/siem/public/graphql/types.ts b/x-pack/legacy/plugins/siem/public/graphql/types.ts
index 73db981ee18e..3246ef1ff49a 100644
--- a/x-pack/legacy/plugins/siem/public/graphql/types.ts
+++ b/x-pack/legacy/plugins/siem/public/graphql/types.ts
@@ -200,7 +200,7 @@ export interface AuthenticationsData {
totalCount: number;
- pageInfo: PageInfo;
+ pageInfo: PageInfoPaginated;
inspect?: Inspect | null;
}
@@ -319,10 +319,12 @@ export interface CursorType {
tiebreaker?: string | null;
}
-export interface PageInfo {
- endCursor?: CursorType | null;
+export interface PageInfoPaginated {
+ activePage: number;
- hasNextPage?: boolean | null;
+ fakeTotalCount: number;
+
+ showMorePagesIndicator: boolean;
}
export interface Inspect {
@@ -799,6 +801,12 @@ export interface SshEcsFields {
signature?: ToStringArray | null;
}
+export interface PageInfo {
+ endCursor?: CursorType | null;
+
+ hasNextPage?: boolean | null;
+}
+
export interface TimelineData {
edges: TimelineEdges[];
@@ -1530,6 +1538,17 @@ export interface TimerangeInput {
from: number;
}
+export interface PaginationInputPaginated {
+ /** The activePage parameter defines the page of results you want to fetch */
+ activePage: number;
+ /** The cursorStart parameter defines the start of the results to be displayed */
+ cursorStart: number;
+ /** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */
+ fakePossibleCount: number;
+ /** The querySize parameter is the number of items to be returned */
+ querySize: number;
+}
+
export interface PaginationInput {
/** The limit parameter allows you to configure the maximum amount of items to be returned */
limit: number;
@@ -1755,7 +1774,7 @@ export interface GetAllTimelineQueryArgs {
export interface AuthenticationsSourceArgs {
timerange: TimerangeInput;
- pagination: PaginationInput;
+ pagination: PaginationInputPaginated;
filterQuery?: string | null;
@@ -2125,7 +2144,7 @@ export namespace GetAuthenticationsQuery {
export type Variables = {
sourceId: string;
timerange: TimerangeInput;
- pagination: PaginationInput;
+ pagination: PaginationInputPaginated;
filterQuery?: string | null;
defaultIndex: string[];
inspect: boolean;
@@ -2242,17 +2261,13 @@ export namespace GetAuthenticationsQuery {
};
export type PageInfo = {
- __typename?: 'PageInfo';
+ __typename?: 'PageInfoPaginated';
- endCursor?: EndCursor | null;
+ activePage: number;
- hasNextPage?: boolean | null;
- };
+ fakeTotalCount: number;
- export type EndCursor = {
- __typename?: 'CursorType';
-
- value?: string | null;
+ showMorePagesIndicator: boolean;
};
export type Inspect = {
diff --git a/x-pack/legacy/plugins/siem/public/mock/global_state.ts b/x-pack/legacy/plugins/siem/public/mock/global_state.ts
index 115804d0e4cd..e52d3d95dd1f 100644
--- a/x-pack/legacy/plugins/siem/public/mock/global_state.ts
+++ b/x-pack/legacy/plugins/siem/public/mock/global_state.ts
@@ -31,7 +31,7 @@ export const mockGlobalState: State = {
hosts: {
page: {
queries: {
- authentications: { limit: 10 },
+ authentications: { activePage: 0, limit: 10 },
hosts: {
limit: 10,
direction: Direction.desc,
@@ -45,7 +45,7 @@ export const mockGlobalState: State = {
},
details: {
queries: {
- authentications: { limit: 10 },
+ authentications: { activePage: 0, limit: 10 },
hosts: {
limit: 10,
direction: Direction.desc,
diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/host_details.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/host_details.tsx
index 2c944f13ffba..9c7c99fef626 100644
--- a/x-pack/legacy/plugins/siem/public/pages/hosts/host_details.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/hosts/host_details.tsx
@@ -169,22 +169,26 @@ const HostDetailsComponent = pure(
totalCount,
loading,
pageInfo,
- loadMore,
+ loadPage,
id,
inspect,
refetch,
}) => (
)}
diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx
index 982b92072e92..cee1b4bcaa7c 100644
--- a/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx
+++ b/x-pack/legacy/plugins/siem/public/pages/hosts/hosts.tsx
@@ -151,22 +151,22 @@ const HostsComponent = pure(({ filterQuery, setAbsoluteRang
totalCount,
loading,
pageInfo,
- loadMore,
+ loadPage,
id,
inspect,
refetch,
}) => (
)}
diff --git a/x-pack/legacy/plugins/siem/public/store/constants.ts b/x-pack/legacy/plugins/siem/public/store/constants.ts
index 27718a809154..d8f13efd12dc 100644
--- a/x-pack/legacy/plugins/siem/public/store/constants.ts
+++ b/x-pack/legacy/plugins/siem/public/store/constants.ts
@@ -5,3 +5,4 @@
*/
export const DEFAULT_TABLE_LIMIT = 10;
+export const DEFAULT_TABLE_ACTIVE_PAGE = 0;
diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/actions.ts b/x-pack/legacy/plugins/siem/public/store/hosts/actions.ts
index 0d6e74c0103e..2d1d5bd35e1b 100644
--- a/x-pack/legacy/plugins/siem/public/store/hosts/actions.ts
+++ b/x-pack/legacy/plugins/siem/public/store/hosts/actions.ts
@@ -9,10 +9,21 @@ import actionCreatorFactory from 'typescript-fsa';
import { HostsSortField } from '../../graphql/types';
import { KueryFilterQuery, SerializedFilterQuery } from '../model';
-import { HostsType } from './model';
-
+import { HostsTableType, HostsType } from './model';
const actionCreator = actionCreatorFactory('x-pack/siem/local/hosts');
+export const updateTableActivePage = actionCreator<{
+ activePage: number;
+ hostsType: HostsType;
+ tableType: HostsTableType;
+}>('UPDATE_HOST_TABLE_ACTIVE_PAGE');
+
+export const updateTableLimit = actionCreator<{
+ hostsType: HostsType;
+ limit: number;
+ tableType: HostsTableType;
+}>('UPDATE_HOST_TABLE_LIMIT');
+
export const updateAuthenticationsLimit = actionCreator<{ limit: number; hostsType: HostsType }>(
'UPDATE_AUTHENTICATIONS_LIMIT'
);
diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/model.ts b/x-pack/legacy/plugins/siem/public/store/hosts/model.ts
index 3cd00c7315c5..e9a587727d20 100644
--- a/x-pack/legacy/plugins/siem/public/store/hosts/model.ts
+++ b/x-pack/legacy/plugins/siem/public/store/hosts/model.ts
@@ -12,17 +12,29 @@ export enum HostsType {
details = 'details',
}
+export enum HostsTableType {
+ authentications = 'authentications',
+ hosts = 'hosts',
+ events = 'events',
+ uncommonProcesses = 'uncommonProcesses',
+}
+
export interface BasicQuery {
limit: number;
}
+export interface BasicQueryPaginated {
+ activePage: number;
+ limit: number;
+}
+
export interface HostsQuery extends BasicQuery {
direction: Direction;
sortField: HostsFields;
}
interface Queries {
- authentications: BasicQuery;
+ authentications: BasicQueryPaginated;
hosts: HostsQuery;
events: BasicQuery;
uncommonProcesses: BasicQuery;
diff --git a/x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts b/x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts
index 2790b86c913d..99df07758a4a 100644
--- a/x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts
+++ b/x-pack/legacy/plugins/siem/public/store/hosts/reducer.ts
@@ -7,7 +7,7 @@
import { reducerWithInitialState } from 'typescript-fsa-reducers';
import { Direction, HostsFields } from '../../graphql/types';
-import { DEFAULT_TABLE_LIMIT } from '../constants';
+import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../constants';
import {
applyHostsFilterQuery,
@@ -17,6 +17,8 @@ import {
updateHostsLimit,
updateHostsSort,
updateUncommonProcessesLimit,
+ updateTableActivePage,
+ updateTableLimit,
} from './actions';
import { HostsModel } from './model';
@@ -25,7 +27,7 @@ export type HostsState = HostsModel;
export const initialHostsState: HostsState = {
page: {
queries: {
- authentications: { limit: DEFAULT_TABLE_LIMIT },
+ authentications: { limit: DEFAULT_TABLE_LIMIT, activePage: DEFAULT_TABLE_ACTIVE_PAGE },
hosts: {
limit: DEFAULT_TABLE_LIMIT,
direction: Direction.desc,
@@ -39,7 +41,7 @@ export const initialHostsState: HostsState = {
},
details: {
queries: {
- authentications: { limit: DEFAULT_TABLE_LIMIT },
+ authentications: { limit: DEFAULT_TABLE_LIMIT, activePage: DEFAULT_TABLE_ACTIVE_PAGE },
hosts: {
limit: DEFAULT_TABLE_LIMIT,
direction: Direction.desc,
@@ -54,6 +56,32 @@ export const initialHostsState: HostsState = {
};
export const hostsReducer = reducerWithInitialState(initialHostsState)
+ .case(updateTableActivePage, (state, { activePage, hostsType, tableType }) => ({
+ ...state,
+ [hostsType]: {
+ ...state[hostsType],
+ queries: {
+ ...state[hostsType].queries,
+ [tableType]: {
+ ...state[hostsType].queries[tableType],
+ activePage,
+ },
+ },
+ },
+ }))
+ .case(updateTableLimit, (state, { limit, hostsType, tableType }) => ({
+ ...state,
+ [hostsType]: {
+ ...state[hostsType],
+ queries: {
+ ...state[hostsType].queries,
+ [tableType]: {
+ ...state[hostsType].queries[tableType],
+ limit,
+ },
+ },
+ },
+ }))
.case(updateAuthenticationsLimit, (state, { limit, hostsType }) => ({
...state,
[hostsType]: {
diff --git a/x-pack/legacy/plugins/siem/server/graphql/authentications/resolvers.ts b/x-pack/legacy/plugins/siem/server/graphql/authentications/resolvers.ts
index f60a870c8d16..b66ccd9a111b 100644
--- a/x-pack/legacy/plugins/siem/server/graphql/authentications/resolvers.ts
+++ b/x-pack/legacy/plugins/siem/server/graphql/authentications/resolvers.ts
@@ -7,7 +7,7 @@
import { SourceResolvers } from '../../graphql/types';
import { Authentications } from '../../lib/authentications';
import { AppResolverOf, ChildResolverOf } from '../../lib/framework';
-import { createOptions } from '../../utils/build_query/create_options';
+import { createOptionsPaginated } from '../../utils/build_query/create_options';
import { QuerySourceResolver } from '../sources/resolvers';
type QueryAuthenticationsResolver = ChildResolverOf<
@@ -28,7 +28,7 @@ export const createAuthenticationsResolvers = (
} => ({
Source: {
async Authentications(source, args, { req }, info) {
- const options = createOptions(source, args, info);
+ const options = createOptionsPaginated(source, args, info);
return libs.authentications.getAuthentications(req, options);
},
},
diff --git a/x-pack/legacy/plugins/siem/server/graphql/authentications/schema.gql.ts b/x-pack/legacy/plugins/siem/server/graphql/authentications/schema.gql.ts
index aa84756ef735..20935ce9ed03 100644
--- a/x-pack/legacy/plugins/siem/server/graphql/authentications/schema.gql.ts
+++ b/x-pack/legacy/plugins/siem/server/graphql/authentications/schema.gql.ts
@@ -30,7 +30,7 @@ export const authenticationsSchema = gql`
type AuthenticationsData {
edges: [AuthenticationsEdges!]!
totalCount: Float!
- pageInfo: PageInfo!
+ pageInfo: PageInfoPaginated!
inspect: Inspect
}
@@ -38,7 +38,7 @@ export const authenticationsSchema = gql`
"Gets Authentication success and failures based on a timerange"
Authentications(
timerange: TimerangeInput!
- pagination: PaginationInput!
+ pagination: PaginationInputPaginated!
filterQuery: String
defaultIndex: [String!]!
): AuthenticationsData!
diff --git a/x-pack/legacy/plugins/siem/server/graphql/types.ts b/x-pack/legacy/plugins/siem/server/graphql/types.ts
index 99335786af75..1fd83d65b262 100644
--- a/x-pack/legacy/plugins/siem/server/graphql/types.ts
+++ b/x-pack/legacy/plugins/siem/server/graphql/types.ts
@@ -229,7 +229,7 @@ export interface AuthenticationsData {
totalCount: number;
- pageInfo: PageInfo;
+ pageInfo: PageInfoPaginated;
inspect?: Inspect | null;
}
@@ -348,10 +348,12 @@ export interface CursorType {
tiebreaker?: string | null;
}
-export interface PageInfo {
- endCursor?: CursorType | null;
+export interface PageInfoPaginated {
+ activePage: number;
- hasNextPage?: boolean | null;
+ fakeTotalCount: number;
+
+ showMorePagesIndicator: boolean;
}
export interface Inspect {
@@ -828,6 +830,12 @@ export interface SshEcsFields {
signature?: ToStringArray | null;
}
+export interface PageInfo {
+ endCursor?: CursorType | null;
+
+ hasNextPage?: boolean | null;
+}
+
export interface TimelineData {
edges: TimelineEdges[];
@@ -1559,6 +1567,17 @@ export interface TimerangeInput {
from: number;
}
+export interface PaginationInputPaginated {
+ /** The activePage parameter defines the page of results you want to fetch */
+ activePage: number;
+ /** The cursorStart parameter defines the start of the results to be displayed */
+ cursorStart: number;
+ /** The fakePossibleCount parameter determines the total count in order to show 5 additional pages */
+ fakePossibleCount: number;
+ /** The querySize parameter is the number of items to be returned */
+ querySize: number;
+}
+
export interface PaginationInput {
/** The limit parameter allows you to configure the maximum amount of items to be returned */
limit: number;
@@ -1784,7 +1803,7 @@ export interface GetAllTimelineQueryArgs {
export interface AuthenticationsSourceArgs {
timerange: TimerangeInput;
- pagination: PaginationInput;
+ pagination: PaginationInputPaginated;
filterQuery?: string | null;
@@ -2503,7 +2522,7 @@ export namespace SourceResolvers {
export interface AuthenticationsArgs {
timerange: TimerangeInput;
- pagination: PaginationInput;
+ pagination: PaginationInputPaginated;
filterQuery?: string | null;
@@ -3012,7 +3031,7 @@ export namespace AuthenticationsDataResolvers {
totalCount?: TotalCountResolver;
- pageInfo?: PageInfoResolver;
+ pageInfo?: PageInfoResolver;
inspect?: InspectResolver;
}
@@ -3028,7 +3047,7 @@ export namespace AuthenticationsDataResolvers {
Context = SiemContext
> = Resolver;
export type PageInfoResolver<
- R = PageInfo,
+ R = PageInfoPaginated,
Parent = AuthenticationsData,
Context = SiemContext
> = Resolver;
@@ -3418,21 +3437,28 @@ export namespace CursorTypeResolvers {
> = Resolver;
}
-export namespace PageInfoResolvers {
- export interface Resolvers {
- endCursor?: EndCursorResolver;
+export namespace PageInfoPaginatedResolvers {
+ export interface Resolvers {
+ activePage?: ActivePageResolver;
- hasNextPage?: HasNextPageResolver;
+ fakeTotalCount?: FakeTotalCountResolver;
+
+ showMorePagesIndicator?: ShowMorePagesIndicatorResolver;
}
- export type EndCursorResolver<
- R = CursorType | null,
- Parent = PageInfo,
+ export type ActivePageResolver<
+ R = number,
+ Parent = PageInfoPaginated,
Context = SiemContext
> = Resolver;
- export type HasNextPageResolver<
- R = boolean | null,
- Parent = PageInfo,
+ export type FakeTotalCountResolver<
+ R = number,
+ Parent = PageInfoPaginated,
+ Context = SiemContext
+ > = Resolver;
+ export type ShowMorePagesIndicatorResolver<
+ R = boolean,
+ Parent = PageInfoPaginated,
Context = SiemContext
> = Resolver;
}
@@ -5024,6 +5050,25 @@ export namespace SshEcsFieldsResolvers {
> = Resolver;
}
+export namespace PageInfoResolvers {
+ export interface Resolvers {
+ endCursor?: EndCursorResolver;
+
+ hasNextPage?: HasNextPageResolver;
+ }
+
+ export type EndCursorResolver<
+ R = CursorType | null,
+ Parent = PageInfo,
+ Context = SiemContext
+ > = Resolver;
+ export type HasNextPageResolver<
+ R = boolean | null,
+ Parent = PageInfo,
+ Context = SiemContext
+ > = Resolver;
+}
+
export namespace TimelineDataResolvers {
export interface Resolvers {
edges?: EdgesResolver;
diff --git a/x-pack/legacy/plugins/siem/server/lib/authentications/elasticsearch_adapter.ts b/x-pack/legacy/plugins/siem/server/lib/authentications/elasticsearch_adapter.ts
index 3e89011e7758..79f13ce4461e 100644
--- a/x-pack/legacy/plugins/siem/server/lib/authentications/elasticsearch_adapter.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/authentications/elasticsearch_adapter.ts
@@ -8,8 +8,9 @@ import { getOr } from 'lodash/fp';
import { AuthenticationsData, AuthenticationsEdges } from '../../graphql/types';
import { mergeFieldsWithHit, inspectStringifyObject } from '../../utils/build_query';
-import { FrameworkAdapter, FrameworkRequest, RequestOptions } from '../framework';
+import { FrameworkAdapter, FrameworkRequest, RequestOptionsPaginated } from '../framework';
import { TermAggregation } from '../types';
+import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants';
import { auditdFieldsMap, buildQuery } from './query.dsl';
import {
@@ -24,16 +25,20 @@ export class ElasticsearchAuthenticationAdapter implements AuthenticationsAdapte
public async getAuthentications(
request: FrameworkRequest,
- options: RequestOptions
+ options: RequestOptionsPaginated
): Promise {
const dsl = buildQuery(options);
+ if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) {
+ throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`);
+ }
const response = await this.framework.callWithRequest(
request,
'search',
dsl
);
- const { cursor, limit } = options.pagination;
+ const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination;
const totalCount = getOr(0, 'aggregations.user_count.value', response);
+ const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount;
const hits: AuthenticationHit[] = getOr(
[],
'aggregations.group_by_users.buckets',
@@ -49,32 +54,28 @@ export class ElasticsearchAuthenticationAdapter implements AuthenticationsAdapte
lastFailure: getOr(null, 'failures.lastFailure.hits.hits[0]._source', bucket),
},
user: bucket.key,
- cursor: bucket.key.user_uid,
failures: bucket.failures.doc_count,
successes: bucket.successes.doc_count,
}));
-
const authenticationEdges: AuthenticationsEdges[] = hits.map(hit =>
formatAuthenticationData(options.fields, hit, auditdFieldsMap)
);
- const hasNextPage = authenticationEdges.length === limit + 1;
- const beginning = cursor != null ? parseInt(cursor!, 10) : 0;
- const edges = authenticationEdges.splice(beginning, limit - beginning);
+ const edges = authenticationEdges.splice(cursorStart, querySize - cursorStart);
const inspect = {
dsl: [inspectStringifyObject(dsl)],
response: [inspectStringifyObject(response)],
};
+ const showMorePagesIndicator = totalCount > fakeTotalCount;
+
return {
inspect,
edges,
totalCount,
pageInfo: {
- hasNextPage,
- endCursor: {
- value: String(limit),
- tiebreaker: null,
- },
+ activePage: activePage ? activePage : 0,
+ fakeTotalCount,
+ showMorePagesIndicator,
},
};
}
diff --git a/x-pack/legacy/plugins/siem/server/lib/authentications/index.ts b/x-pack/legacy/plugins/siem/server/lib/authentications/index.ts
index 4ed0c9b1ed14..d64f820de239 100644
--- a/x-pack/legacy/plugins/siem/server/lib/authentications/index.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/authentications/index.ts
@@ -5,7 +5,7 @@
*/
import { AuthenticationsData } from '../../graphql/types';
-import { FrameworkRequest, RequestOptions } from '../framework';
+import { FrameworkRequest, RequestOptionsPaginated } from '../framework';
import { AuthenticationsAdapter } from './types';
@@ -14,7 +14,7 @@ export class Authentications {
public async getAuthentications(
req: FrameworkRequest,
- options: RequestOptions
+ options: RequestOptionsPaginated
): Promise {
return await this.adapter.getAuthentications(req, options);
}
diff --git a/x-pack/legacy/plugins/siem/server/lib/authentications/query.dsl.ts b/x-pack/legacy/plugins/siem/server/lib/authentications/query.dsl.ts
index ae184c321299..333cc79fadab 100644
--- a/x-pack/legacy/plugins/siem/server/lib/authentications/query.dsl.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/authentications/query.dsl.ts
@@ -8,7 +8,7 @@ import { createQueryFilterClauses } from '../../utils/build_query';
import { reduceFields } from '../../utils/build_query/reduce_fields';
import { hostFieldsMap, sourceFieldsMap } from '../ecs_fields';
import { extendMap } from '../ecs_fields/extend_map';
-import { RequestOptions } from '../framework';
+import { RequestOptionsPaginated } from '../framework';
export const auditdFieldsMap: Readonly> = {
latest: '@timestamp',
@@ -24,12 +24,12 @@ export const buildQuery = ({
fields,
filterQuery,
timerange: { from, to },
- pagination: { limit },
+ pagination: { querySize },
defaultIndex,
sourceConfiguration: {
fields: { timestamp },
},
-}: RequestOptions) => {
+}: RequestOptionsPaginated) => {
const esFields = reduceFields(fields, { ...hostFieldsMap, ...sourceFieldsMap });
const filter = [
@@ -62,7 +62,7 @@ export const buildQuery = ({
...agg,
group_by_users: {
terms: {
- size: limit + 1,
+ size: querySize,
field: 'user.name',
order: [{ 'successes.doc_count': 'desc' }, { 'failures.doc_count': 'desc' }],
},
@@ -107,8 +107,8 @@ export const buildQuery = ({
filter,
},
},
+ size: 0,
},
- size: 0,
track_total_hits: false,
};
diff --git a/x-pack/legacy/plugins/siem/server/lib/authentications/types.ts b/x-pack/legacy/plugins/siem/server/lib/authentications/types.ts
index f120bcdfbf50..2d2c7ba547c0 100644
--- a/x-pack/legacy/plugins/siem/server/lib/authentications/types.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/authentications/types.ts
@@ -5,11 +5,14 @@
*/
import { AuthenticationsData, LastSourceHost } from '../../graphql/types';
-import { FrameworkRequest, RequestOptions } from '../framework';
+import { FrameworkRequest, RequestOptionsPaginated } from '../framework';
import { Hit, SearchHit, TotalHit } from '../types';
export interface AuthenticationsAdapter {
- getAuthentications(req: FrameworkRequest, options: RequestOptions): Promise;
+ getAuthentications(
+ req: FrameworkRequest,
+ options: RequestOptionsPaginated
+ ): Promise;
}
type StringOrNumber = string | number;
diff --git a/x-pack/legacy/plugins/siem/server/lib/framework/types.ts b/x-pack/legacy/plugins/siem/server/lib/framework/types.ts
index 16fd3c4f0ba5..559915776311 100644
--- a/x-pack/legacy/plugins/siem/server/lib/framework/types.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/framework/types.ts
@@ -12,6 +12,7 @@ import { Legacy } from 'kibana';
import { ESQuery } from '../../../common/typed_json';
import {
PaginationInput,
+ PaginationInputPaginated,
SortField,
SourceConfiguration,
TimerangeInput,
@@ -159,3 +160,9 @@ export interface RequestOptions extends RequestBasicOptions {
fields: readonly string[];
sortField?: SortField;
}
+
+export interface RequestOptionsPaginated extends RequestBasicOptions {
+ pagination: PaginationInputPaginated;
+ fields: ReadonlyArray;
+ sortField?: SortField;
+}
diff --git a/x-pack/legacy/plugins/siem/server/utils/build_query/create_options.ts b/x-pack/legacy/plugins/siem/server/utils/build_query/create_options.ts
index f3c7832fb609..5210060c7c7b 100644
--- a/x-pack/legacy/plugins/siem/server/utils/build_query/create_options.ts
+++ b/x-pack/legacy/plugins/siem/server/utils/build_query/create_options.ts
@@ -7,8 +7,14 @@
import { GraphQLResolveInfo } from 'graphql';
import { getOr } from 'lodash/fp';
-import { PaginationInput, SortField, Source, TimerangeInput } from '../../graphql/types';
-import { RequestOptions } from '../../lib/framework';
+import {
+ PaginationInput,
+ PaginationInputPaginated,
+ SortField,
+ Source,
+ TimerangeInput,
+} from '../../graphql/types';
+import { RequestOptions, RequestOptionsPaginated } from '../../lib/framework';
import { parseFilterQuery } from '../serialized_query';
import { getFields } from '.';
@@ -27,6 +33,13 @@ export interface Args {
sortField?: SortField | null;
defaultIndex: string[];
}
+export interface ArgsPaginated {
+ timerange?: TimerangeInput | null;
+ pagination?: PaginationInputPaginated | null;
+ filterQuery?: string | null;
+ sortField?: SortField | null;
+ defaultIndex: string[];
+}
export const createOptions = (
source: Configuration,
@@ -47,3 +60,23 @@ export const createOptions = (
.map(field => field.replace(fieldReplacement, '')),
};
};
+
+export const createOptionsPaginated = (
+ source: Configuration,
+ args: ArgsPaginated,
+ info: FieldNodes,
+ fieldReplacement: string = 'edges.node.'
+): RequestOptionsPaginated => {
+ const fields = getFields(getOr([], 'fieldNodes[0]', info));
+ return {
+ defaultIndex: args.defaultIndex,
+ sourceConfiguration: source.configuration,
+ timerange: args.timerange!,
+ pagination: args.pagination!,
+ sortField: args.sortField!,
+ filterQuery: parseFilterQuery(args.filterQuery || ''),
+ fields: fields
+ .filter(field => !field.includes('__typename'))
+ .map(field => field.replace(fieldReplacement, '')),
+ };
+};
diff --git a/x-pack/test/api_integration/apis/siem/authentications.ts b/x-pack/test/api_integration/apis/siem/authentications.ts
index d4de9ec389b0..1ac36d31e43b 100644
--- a/x-pack/test/api_integration/apis/siem/authentications.ts
+++ b/x-pack/test/api_integration/apis/siem/authentications.ts
@@ -38,8 +38,10 @@ const authenticationsTests: KbnTestProvider = ({ getService }) => {
from: FROM,
},
pagination: {
- limit: 1,
- cursor: null,
+ activePage: 0,
+ cursorStart: 0,
+ fakePossibleCount: 3,
+ querySize: 1,
},
defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
inspect: false,
@@ -49,7 +51,7 @@ const authenticationsTests: KbnTestProvider = ({ getService }) => {
const authentications = resp.data.source.Authentications;
expect(authentications.edges.length).to.be(EDGE_LENGTH);
expect(authentications.totalCount).to.be(TOTAL_COUNT);
- expect(authentications.pageInfo.endCursor!.value).to.equal('1');
+ expect(authentications.pageInfo.fakeTotalCount).to.equal(3);
});
});
@@ -65,8 +67,10 @@ const authenticationsTests: KbnTestProvider = ({ getService }) => {
from: FROM,
},
pagination: {
- limit: 2,
- cursor: '1',
+ activePage: 2,
+ cursorStart: 1,
+ fakePossibleCount: 5,
+ querySize: 2,
},
defaultIndex: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
inspect: false,