mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[SIEM] - Authentications Table, Numbered Pagination (#39474)
[SIEM] - Authentications Table, Numbered Pagination
This commit is contained in:
parent
3a13b7ce23
commit
bfee75ea6c
39 changed files with 1720 additions and 184 deletions
|
@ -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;
|
||||
|
|
|
@ -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!
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -87,6 +87,7 @@ describe('AddToKql Component', async () => {
|
|||
expect(store.getState().hosts.page).toEqual({
|
||||
queries: {
|
||||
authentications: {
|
||||
activePage: 0,
|
||||
limit: 10,
|
||||
},
|
||||
hosts: {
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
`;
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
107
x-pack/legacy/plugins/siem/public/components/paginated_table/__snapshots__/index.test.tsx.snap
generated
Normal file
107
x-pack/legacy/plugins/siem/public/components/paginated_table/__snapshots__/index.test.tsx.snap
generated
Normal 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]}
|
||||
/>
|
||||
`;
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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,
|
||||
},
|
||||
];
|
|
@ -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' } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
`;
|
|
@ -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',
|
||||
});
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -5,3 +5,4 @@
|
|||
*/
|
||||
|
||||
export const DEFAULT_TABLE_LIMIT = 10;
|
||||
export const DEFAULT_TABLE_ACTIVE_PAGE = 0;
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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]: {
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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, '')),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue