[SIEM] - Authentications Table, Numbered Pagination (#39474)

[SIEM] - Authentications Table, Numbered Pagination
This commit is contained in:
Steph Milovic 2019-07-11 14:56:06 -06:00 committed by GitHub
parent 3a13b7ce23
commit bfee75ea6c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 1720 additions and 184 deletions

View file

View file

@ -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;

View file

@ -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!
}
`;

View file

@ -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};

View file

@ -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 &&

View file

@ -87,6 +87,7 @@ describe('AddToKql Component', async () => {
expect(store.getState().hosts.page).toEqual({
queries: {
authentications: {
activePage: 0,
limit: 10,
},
hosts: {

View file

@ -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"
/>
`;

View file

@ -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', () => {
<ReduxStoreProvider store={store}>
<AuthenticationTable
data={mockData.Authentications.edges}
hasNextPage={getOr(false, 'hasNextPage', mockData.Authentications.pageInfo)!}
fakeTotalCount={getOr(50, 'fakeTotalCount', mockData.Authentications.pageInfo)!}
id="authentication"
loading={false}
loadMore={loadMore}
nextCursor={getOr(null, 'endCursor.value', mockData.Authentications.pageInfo)}
loadPage={loadPage}
showMorePagesIndicator={getOr(
false,
'showMorePagesIndicator',
mockData.Authentications.pageInfo
)}
totalCount={mockData.Authentications.totalCount}
type={hostsModel.HostsType.page}
/>

View file

@ -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<AuthenticationsEdges>,
Columns<AuthenticationsEdges>,
Columns<AuthenticationsEdges>,
Columns<AuthenticationsEdges>,
Columns<AuthenticationsEdges>,
Columns<AuthenticationsEdges>,
Columns<AuthenticationsEdges>,
Columns<AuthenticationsEdges>,
Columns<AuthenticationsEdges>
];
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<AuthenticationTableProps>(
({
fakeTotalCount,
data,
hasNextPage,
id,
limit,
loading,
loadMore,
loadPage,
showMorePagesIndicator,
totalCount,
nextCursor,
updateLimitPagination,
type,
updateTableActivePage,
updateTableLimit,
}) => (
<LoadMoreTable
<PaginatedTable
columns={getAuthenticationColumnsCurated(type)}
hasNextPage={hasNextPage}
headerCount={totalCount}
headerTitle={i18n.AUTHENTICATIONS}
headerUnit={i18n.UNIT(totalCount)}
@ -92,11 +108,25 @@ const AuthenticationTableComponent = pure<AuthenticationTableProps>(
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<AuthenticationsEdges>,
Columns<AuthenticationsEdges>,
Columns<AuthenticationsEdges>,
Columns<AuthenticationsEdges>,
Columns<AuthenticationsEdges>,
Columns<AuthenticationsEdges>,
Columns<AuthenticationsEdges>,
Columns<AuthenticationsEdges>,
Columns<AuthenticationsEdges>
] => [
const getAuthenticationColumns = (): AuthTableColumns => [
{
name: i18n.USER,
truncateText: false,

View file

@ -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,
},
},
};

View file

@ -0,0 +1,107 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Paginated Table Component rendering it renders the default load more table 1`] = `
<Component
columns={
Array [
Object {
"field": "node.host.name",
"hideForMobile": false,
"name": "Host",
"render": [Function],
"truncateText": false,
},
Object {
"field": "node.host.firstSeen",
"hideForMobile": false,
"name": "First seen",
"render": [Function],
"truncateText": false,
},
Object {
"field": "node.host.os",
"hideForMobile": false,
"name": "OS",
"render": [Function],
"truncateText": false,
},
Object {
"field": "node.host.version",
"hideForMobile": false,
"name": "Version",
"render": [Function],
"truncateText": false,
},
]
}
headerCount={1}
headerSupplement={
<p>
My test supplement.
</p>
}
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]}
/>
`;

View file

@ -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,
});
});
});

View file

@ -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,
};
};

View file

@ -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<string>,
Columns<string>,
Columns<string>,
Columns<string>
] => [
{
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<string>,
Columns<string>,
Columns<string>,
Columns<string>
] = getHostsColumns().map(h => ({ ...h, sortable: true })) as [
Columns<string>,
Columns<string>,
Columns<string>,
Columns<string>
];
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,
},
];

View file

@ -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<number>;
let updateLimitPagination: jest.Mock<number>;
let updateActivePage: jest.Mock<number>;
beforeEach(() => {
loadPage = jest.fn();
updateLimitPagination = jest.fn();
updateActivePage = jest.fn();
});
describe('rendering', () => {
test('it renders the default load more table', () => {
const wrapper = shallow(
<ThemeProvider theme={theme}>
<PaginatedTable
columns={getHostsColumns()}
headerCount={1}
headerSupplement={<p>{'My test supplement.'}</p>}
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 })}
/>
</ThemeProvider>
);
expect(toJson(wrapper)).toMatchSnapshot();
});
test('it renders the loading panel at the beginning ', () => {
const wrapper = mount(
<ThemeProvider theme={theme}>
<PaginatedTable
columns={getHostsColumns()}
headerCount={1}
headerSupplement={<p>{'My test supplement.'}</p>}
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 })}
/>
</ThemeProvider>
);
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(
<ThemeProvider theme={theme}>
<PaginatedTable
columns={getHostsColumns()}
headerCount={1}
headerSupplement={<p>{'My test supplement.'}</p>}
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 })}
/>
</ThemeProvider>
);
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(
<ThemeProvider theme={theme}>
<PaginatedTable
columns={getHostsColumns()}
headerCount={1}
headerSupplement={<p>{'My test supplement.'}</p>}
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 })}
/>
</ThemeProvider>
);
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(
<ThemeProvider theme={theme}>
<PaginatedTable
columns={getHostsColumns()}
headerCount={1}
headerSupplement={<p>{'My test supplement.'}</p>}
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 })}
/>
</ThemeProvider>
);
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(
<ThemeProvider theme={theme}>
<PaginatedTable
columns={getHostsColumns()}
headerCount={1}
headerSupplement={<p>{'My test supplement.'}</p>}
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 })}
/>
</ThemeProvider>
);
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(
<ThemeProvider theme={theme}>
<PaginatedTable
columns={sortedHosts}
headerCount={1}
headerSupplement={<p>{'My test supplement.'}</p>}
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 })}
/>
</ThemeProvider>
);
expect(wrapper.find('.euiTable thead tr th button svg')).toBeTruthy();
});
test('Should display toast when user reaches end of results max', () => {
const wrapper = mount(
<ThemeProvider theme={theme}>
<PaginatedTable
columns={getHostsColumns()}
headerCount={1}
headerSupplement={<p>{'My test supplement.'}</p>}
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 })}
/>
</ThemeProvider>
);
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(
<ThemeProvider theme={theme}>
<PaginatedTable
columns={getHostsColumns()}
headerCount={1}
headerSupplement={<p>{'My test supplement.'}</p>}
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 })}
/>
</ThemeProvider>
);
expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeTruthy();
});
test('Should hide items per row if totalCount is less than items', () => {
const wrapper = mount(
<ThemeProvider theme={theme}>
<PaginatedTable
columns={getHostsColumns()}
headerCount={1}
headerSupplement={<p>{'My test supplement.'}</p>}
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 })}
/>
</ThemeProvider>
);
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(
<ThemeProvider theme={theme}>
<PaginatedTable
columns={getHostsColumns()}
headerCount={1}
headerSupplement={<p>{'My test supplement.'}</p>}
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 })}
/>
</ThemeProvider>
);
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(
<ThemeProvider theme={theme}>
<PaginatedTable
columns={getHostsColumns()}
headerCount={1}
headerSupplement={<p>{'My test supplement.'}</p>}
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 })}
/>
</ThemeProvider>
);
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<any> = {
columns: getHostsColumns(),
headerCount: 1,
headerSupplement: <p>{'My test supplement.'}</p>,
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<any>) => {
return (
<ThemeProvider theme={theme}>
<PaginatedTable {...props} />
</ThemeProvider>
);
};
const wrapper = mount(<ComponentWithContext {...ourProps} />);
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(
<ThemeProvider theme={theme}>
<PaginatedTable
columns={getHostsColumns()}
headerCount={1}
headerSupplement={<p>{'My test supplement.'}</p>}
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 })}
/>
</ThemeProvider>
);
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(
<ThemeProvider theme={theme}>
<PaginatedTable
columns={sortedHosts}
headerCount={1}
headerSupplement={<p>{'My test supplement.'}</p>}
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 })}
/>
</ThemeProvider>
);
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' } },
]);
});
});
});

View file

@ -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<string>,
Columns<string>,
Columns<string>,
Columns<string>
];
declare type BasicTableColumns = AuthTableColumns | HostsTableColumns;
declare type SiemTables = BasicTableProps<BasicTableColumns>;
// Using telescoping templates to remove 'any' that was polluting downstream column type checks
export interface BasicTableProps<T> {
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<T> {
field?: string;
name: string | React.ReactNode;
isMobileHeader?: boolean;
sortable?: boolean;
truncateText?: boolean;
hideForMobile?: boolean;
render?: (item: T) => void;
width?: string;
}
export const PaginatedTable = memo<SiemTables>(
({
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 (
<EuiPanel>
<LoadingPanel
height="auto"
width="100%"
text={`${i18n.LOADING} ${loadingTitle ? loadingTitle : headerTitle}`}
data-test-subj="InitialLoadingPanelPaginatedTable"
/>
</EuiPanel>
);
}
const button = (
<EuiButtonEmpty
size="s"
color="text"
iconType="arrowDown"
iconSide="right"
onClick={onButtonClick}
>
{`${i18n.ROWS}: ${limit}`}
</EuiButtonEmpty>
);
const rowItems =
itemsPerRow &&
itemsPerRow.map((item: ItemsPerRow) => (
<EuiContextMenuItem
key={item.text}
icon={limit === item.numberOfRow ? 'check' : 'empty'}
onClick={() => {
closePopover();
updateLimitPagination(item.numberOfRow);
updateActivePage(0); // reset results to first page
}}
>
{item.text}
</EuiContextMenuItem>
));
const PaginationWrapper = showMorePagesIndicator ? PaginationEuiFlexItem : EuiFlexItem;
return (
<EuiPanel
data-test-subj={dataTestSubj}
onMouseEnter={() => setShowInspect(true)}
onMouseLeave={() => setShowInspect(false)}
>
<BasicTableContainer>
{loading && (
<>
<BackgroundRefetch />
<LoadingPanel
height="100%"
width="100%"
text={`${i18n.LOADING} ${loadingTitle ? loadingTitle : headerTitle}`}
position="absolute"
zIndex={3}
data-test-subj="LoadingPanelPaginatedTable"
/>
</>
)}
<HeaderPanel
id={id}
showInspect={showInspect}
subtitle={`${i18n.SHOWING}: ${headerCount.toLocaleString()} ${headerUnit}`}
title={headerTitle}
tooltip={headerTooltip}
>
{headerSupplement}
</HeaderPanel>
<BasicTable
items={pageOfItems}
columns={columns}
onChange={onChange}
sorting={
sorting
? {
sort: {
field: sorting.field,
direction: sorting.direction,
},
}
: null
}
/>
<FooterAction>
<EuiFlexGroup alignItems="center">
<EuiFlexItem>
{itemsPerRow && itemsPerRow.length > 0 && totalCount >= itemsPerRow[0].numberOfRow && (
<EuiPopover
id="customizablePagination"
data-test-subj="loadingMoreSizeRowPopover"
button={button}
isOpen={isPopoverOpen}
closePopover={closePopover}
panelPaddingSize="none"
>
<EuiContextMenuPanel items={rowItems} data-test-subj="loadingMorePickSizeRow" />
</EuiPopover>
)}
</EuiFlexItem>
<PaginationWrapper grow={false}>
<EuiPagination
data-test-subj="numberedPagination"
pageCount={pageCount}
activePage={activePage}
onPageClick={goToPage}
/>
</PaginationWrapper>
</EuiFlexGroup>
</FooterAction>
</BasicTableContainer>
</EuiPanel>
);
}
);
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;
}
`;

View file

@ -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',
});

View file

@ -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

View file

@ -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 {
activePage,
children,
endDate,
filterQuery,
id = ID,
isInspected,
children,
filterQuery,
limit,
skip,
sourceId,
startDate,
endDate,
limit,
} = this.props;
return (
<Query<GetAuthenticationsQuery.Query, GetAuthenticationsQuery.Variables>
@ -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),
});
}}
</Query>

View file

@ -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<TData, TVariables> = FetchMoreQueryOptions<any, any> &
FetchMoreOptions<TData, TVariables>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type PromiseApolloQueryResult = Promise<ApolloQueryResult<any>>;
export class QueryTemplatePaginated<
T extends QueryTemplatePaginatedProps,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TData = any,
TVariables = OperationVariables
> extends React.PureComponent<T, TData, TVariables> {
private fetchMore!: (
fetchMoreOptions: FetchMoreOptionsArgs<TData, TVariables>
) => PromiseApolloQueryResult;
private fetchMoreOptions!: (newActivePage: number) => FetchMoreOptionsArgs<TData, TVariables>;
public constructor(props: T) {
super(props);
}
public setFetchMore = (
val: (fetchMoreOptions: FetchMoreOptionsArgs<TData, TVariables>) => PromiseApolloQueryResult
) => {
this.fetchMore = val;
};
public setFetchMoreOptions = (
val: (newActivePage: number) => FetchMoreOptionsArgs<TData, TVariables>
) => {
this.fetchMoreOptions = val;
};
public wrappedLoadMore = (newActivePage: number) => {
return this.fetchMore(this.fetchMoreOptions(newActivePage));
};
}

View file

@ -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",

View file

@ -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 = {

View file

@ -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,

View file

@ -169,22 +169,26 @@ const HostDetailsComponent = pure<HostDetailsComponentProps>(
totalCount,
loading,
pageInfo,
loadMore,
loadPage,
id,
inspect,
refetch,
}) => (
<AuthenticationTableManage
data={authentications}
fakeTotalCount={getOr(50, 'fakeTotalCount', pageInfo)}
id={id}
inspect={inspect}
refetch={refetch}
setQuery={setQuery}
loading={loading}
data={authentications}
loadPage={loadPage}
refetch={refetch}
showMorePagesIndicator={getOr(
false,
'showMorePagesIndicator',
pageInfo
)}
setQuery={setQuery}
totalCount={totalCount}
nextCursor={getOr(null, 'endCursor.value', pageInfo)}
hasNextPage={getOr(false, 'hasNextPage', pageInfo)!}
loadMore={loadMore}
type={type}
/>
)}

View file

@ -151,22 +151,22 @@ const HostsComponent = pure<HostsComponentProps>(({ filterQuery, setAbsoluteRang
totalCount,
loading,
pageInfo,
loadMore,
loadPage,
id,
inspect,
refetch,
}) => (
<AuthenticationTableManage
data={authentications}
fakeTotalCount={getOr(50, 'fakeTotalCount', pageInfo)}
id={id}
inspect={inspect}
refetch={refetch}
setQuery={setQuery}
loading={loading}
data={authentications}
loadPage={loadPage}
refetch={refetch}
showMorePagesIndicator={getOr(false, 'showMorePagesIndicator', pageInfo)}
setQuery={setQuery}
totalCount={totalCount}
nextCursor={getOr(null, 'endCursor.value', pageInfo)}
hasNextPage={getOr(false, 'hasNextPage', pageInfo)!}
loadMore={loadMore}
type={hostsModel.HostsType.page}
/>
)}

View file

@ -5,3 +5,4 @@
*/
export const DEFAULT_TABLE_LIMIT = 10;
export const DEFAULT_TABLE_ACTIVE_PAGE = 0;

View file

@ -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'
);

View file

@ -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;

View file

@ -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]: {

View file

@ -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);
},
},

View file

@ -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!

View file

@ -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<number, TypeParent, Context>;
pageInfo?: PageInfoResolver<PageInfo, TypeParent, Context>;
pageInfo?: PageInfoResolver<PageInfoPaginated, TypeParent, Context>;
inspect?: InspectResolver<Inspect | null, TypeParent, Context>;
}
@ -3028,7 +3047,7 @@ export namespace AuthenticationsDataResolvers {
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type PageInfoResolver<
R = PageInfo,
R = PageInfoPaginated,
Parent = AuthenticationsData,
Context = SiemContext
> = Resolver<R, Parent, Context>;
@ -3418,21 +3437,28 @@ export namespace CursorTypeResolvers {
> = Resolver<R, Parent, Context>;
}
export namespace PageInfoResolvers {
export interface Resolvers<Context = SiemContext, TypeParent = PageInfo> {
endCursor?: EndCursorResolver<CursorType | null, TypeParent, Context>;
export namespace PageInfoPaginatedResolvers {
export interface Resolvers<Context = SiemContext, TypeParent = PageInfoPaginated> {
activePage?: ActivePageResolver<number, TypeParent, Context>;
hasNextPage?: HasNextPageResolver<boolean | null, TypeParent, Context>;
fakeTotalCount?: FakeTotalCountResolver<number, TypeParent, Context>;
showMorePagesIndicator?: ShowMorePagesIndicatorResolver<boolean, TypeParent, Context>;
}
export type EndCursorResolver<
R = CursorType | null,
Parent = PageInfo,
export type ActivePageResolver<
R = number,
Parent = PageInfoPaginated,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type HasNextPageResolver<
R = boolean | null,
Parent = PageInfo,
export type FakeTotalCountResolver<
R = number,
Parent = PageInfoPaginated,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type ShowMorePagesIndicatorResolver<
R = boolean,
Parent = PageInfoPaginated,
Context = SiemContext
> = Resolver<R, Parent, Context>;
}
@ -5024,6 +5050,25 @@ export namespace SshEcsFieldsResolvers {
> = Resolver<R, Parent, Context>;
}
export namespace PageInfoResolvers {
export interface Resolvers<Context = SiemContext, TypeParent = PageInfo> {
endCursor?: EndCursorResolver<CursorType | null, TypeParent, Context>;
hasNextPage?: HasNextPageResolver<boolean | null, TypeParent, Context>;
}
export type EndCursorResolver<
R = CursorType | null,
Parent = PageInfo,
Context = SiemContext
> = Resolver<R, Parent, Context>;
export type HasNextPageResolver<
R = boolean | null,
Parent = PageInfo,
Context = SiemContext
> = Resolver<R, Parent, Context>;
}
export namespace TimelineDataResolvers {
export interface Resolvers<Context = SiemContext, TypeParent = TimelineData> {
edges?: EdgesResolver<TimelineEdges[], TypeParent, Context>;

View file

@ -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<AuthenticationsData> {
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<AuthenticationData, TermAggregation>(
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,
},
};
}

View file

@ -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<AuthenticationsData> {
return await this.adapter.getAuthentications(req, options);
}

View file

@ -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<Record<string, string>> = {
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,
};

View file

@ -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<AuthenticationsData>;
getAuthentications(
req: FrameworkRequest,
options: RequestOptionsPaginated
): Promise<AuthenticationsData>;
}
type StringOrNumber = string | number;

View file

@ -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<string>;
sortField?: SortField;
}

View file

@ -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, '')),
};
};

View file

@ -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,